mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-28 07:33:16 +00:00
Compare commits
56 Commits
v0.2.5
...
wp/mrdimid
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98124e97aa | ||
|
|
dd1c758c0e | ||
|
|
0d87c352b2 | ||
|
|
918f6ce0e6 | ||
|
|
6c5efe6ce0 | ||
|
|
f0be6675e7 | ||
|
|
6a8174a15c | ||
|
|
40c3f1b618 | ||
|
|
6dd2dac049 | ||
|
|
b39bbb557f | ||
|
|
f7682cba67 | ||
|
|
f94c07160a | ||
|
|
bbe6692580 | ||
|
|
9266a1c4d9 | ||
|
|
220d80f05f | ||
|
|
9144c909dd | ||
|
|
7981fcec84 | ||
|
|
71264c56fc | ||
|
|
ca0f77bdee | ||
|
|
fc8b1b8549 | ||
|
|
bc8c44f62f | ||
|
|
01fab5c92a | ||
|
|
1c07d786a0 | ||
|
|
6f0cd87d1c | ||
|
|
e44308cba2 | ||
|
|
50245c5157 | ||
|
|
9ca5188e12 | ||
|
|
56cc881ac0 | ||
|
|
50896bdc9d | ||
|
|
8dd4567828 | ||
|
|
06ef6d3e6a | ||
|
|
14b58e8062 | ||
|
|
eee232c12c | ||
|
|
febe321aef | ||
|
|
28777ac717 | ||
|
|
13b008b56c | ||
|
|
523efbd85a | ||
|
|
fcacc8bfc6 | ||
|
|
252b3c3bf6 | ||
|
|
24221748e1 | ||
|
|
141ae053db | ||
|
|
10ec4ff814 | ||
|
|
d2da0b7c0e | ||
|
|
7d0548406e | ||
|
|
c121dbbd67 | ||
|
|
c1c0a7d494 | ||
|
|
0749f60702 | ||
|
|
ca0ef18bdf | ||
|
|
6ed011e2f8 | ||
|
|
23d322452a | ||
|
|
5d3b965d28 | ||
|
|
d9794d72c7 | ||
|
|
524b5be937 | ||
|
|
ac2e276a6a | ||
|
|
4f4dbc0c22 | ||
|
|
d56e63a91b |
2
.github/workflows/wpt.yml
vendored
2
.github/workflows/wpt.yml
vendored
@@ -107,7 +107,7 @@ jobs:
|
||||
run: |
|
||||
./wpt serve 2> /dev/null & echo $! > WPT.pid
|
||||
sleep 10s
|
||||
./wptrunner -lpd-path ./lightpanda -json -concurrency 1 > wpt.json
|
||||
./wptrunner -lpd-path ./lightpanda -json -concurrency 3 > wpt.json
|
||||
kill `cat WPT.pid`
|
||||
|
||||
- name: write commit
|
||||
|
||||
362
src/Net.zig
362
src/Net.zig
@@ -174,16 +174,16 @@ const HeaderValue = struct {
|
||||
|
||||
pub const AuthChallenge = struct {
|
||||
status: u16,
|
||||
source: enum { server, proxy },
|
||||
scheme: enum { basic, digest },
|
||||
realm: []const u8,
|
||||
source: ?enum { server, proxy },
|
||||
scheme: ?enum { basic, digest },
|
||||
realm: ?[]const u8,
|
||||
|
||||
pub fn parse(status: u16, header: []const u8) !AuthChallenge {
|
||||
var ac: AuthChallenge = .{
|
||||
.status = status,
|
||||
.source = undefined,
|
||||
.realm = "TODO", // TODO parser and set realm
|
||||
.scheme = undefined,
|
||||
.source = null,
|
||||
.realm = null,
|
||||
.scheme = null,
|
||||
};
|
||||
|
||||
const sep = std.mem.indexOfPos(u8, header, 0, ": ") orelse return error.InvalidHeader;
|
||||
@@ -471,13 +471,22 @@ pub const Connection = struct {
|
||||
|
||||
pub const Handles = struct {
|
||||
connections: []Connection,
|
||||
dirty: HandleList,
|
||||
in_use: HandleList,
|
||||
available: HandleList,
|
||||
multi: *libcurl.CurlM,
|
||||
performing: bool = false,
|
||||
runtime_ctx: ?RuntimeContext = null,
|
||||
|
||||
pub const HandleList = std.DoublyLinkedList;
|
||||
|
||||
const RuntimeContext = struct {
|
||||
handles: *Handles,
|
||||
runtime: *Runtime,
|
||||
cdp_fd: ?posix.fd_t,
|
||||
timer_deadline_ms: ?i64 = null,
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
allocator: Allocator,
|
||||
ca_blob: ?libcurl.CurlBlob,
|
||||
@@ -501,6 +510,7 @@ pub const Handles = struct {
|
||||
}
|
||||
|
||||
return .{
|
||||
.dirty = .{},
|
||||
.in_use = .{},
|
||||
.connections = connections,
|
||||
.available = available,
|
||||
@@ -522,8 +532,6 @@ pub const Handles = struct {
|
||||
|
||||
pub fn get(self: *Handles) ?*Connection {
|
||||
if (self.available.popFirst()) |node| {
|
||||
node.prev = null;
|
||||
node.next = null;
|
||||
self.in_use.append(node);
|
||||
return @as(*Connection, @fieldParentPtr("node", node));
|
||||
}
|
||||
@@ -532,28 +540,88 @@ pub const Handles = struct {
|
||||
|
||||
pub fn add(self: *Handles, conn: *const Connection) !void {
|
||||
try libcurl.curl_multi_add_handle(self.multi, conn.easy);
|
||||
if (self.runtime_ctx != null) {
|
||||
_ = try self.socketActionTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(self: *Handles, conn: *Connection) void {
|
||||
libcurl.curl_multi_remove_handle(self.multi, conn.easy) catch |err| {
|
||||
log.fatal(.http, "multi remove handle", .{ .err = err });
|
||||
};
|
||||
var node = &conn.node;
|
||||
if (libcurl.curl_multi_remove_handle(self.multi, conn.easy)) {
|
||||
self.isAvailable(conn);
|
||||
} else |err| {
|
||||
// can happen if we're in a perform() call, so we'll queue this
|
||||
// for cleanup later.
|
||||
const node = &conn.node;
|
||||
self.in_use.remove(node);
|
||||
self.dirty.append(node);
|
||||
log.warn(.http, "multi remove handle", .{ .err = err });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn isAvailable(self: *Handles, conn: *Connection) void {
|
||||
const node = &conn.node;
|
||||
self.in_use.remove(node);
|
||||
node.prev = null;
|
||||
node.next = null;
|
||||
self.available.append(node);
|
||||
}
|
||||
|
||||
pub fn perform(self: *Handles) !c_int {
|
||||
var running: c_int = undefined;
|
||||
if (self.runtime_ctx != null) {
|
||||
return self.socketActionTimeout();
|
||||
}
|
||||
|
||||
self.performing = true;
|
||||
defer self.performing = false;
|
||||
|
||||
const multi = self.multi;
|
||||
var running: c_int = undefined;
|
||||
try libcurl.curl_multi_perform(self.multi, &running);
|
||||
|
||||
{
|
||||
const list = &self.dirty;
|
||||
while (list.first) |node| {
|
||||
list.remove(node);
|
||||
const conn: *Connection = @fieldParentPtr("node", node);
|
||||
if (libcurl.curl_multi_remove_handle(multi, conn.easy)) {
|
||||
self.available.append(node);
|
||||
} else |err| {
|
||||
log.fatal(.http, "multi remove handle", .{ .err = err, .src = "perform" });
|
||||
@panic("multi_remove_handle");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return running;
|
||||
}
|
||||
|
||||
pub fn poll(self: *Handles, extra_fds: []libcurl.CurlWaitFd, timeout_ms: c_int) !void {
|
||||
if (self.runtime_ctx) |*ctx| {
|
||||
var wait_ms: i32 = @intCast(timeout_ms);
|
||||
try self.runDueTimer();
|
||||
|
||||
if (ctx.timer_deadline_ms) |deadline| {
|
||||
const now = std.time.milliTimestamp();
|
||||
if (deadline > now) {
|
||||
const remaining: i32 = @intCast(@min(deadline - now, std.math.maxInt(i32)));
|
||||
wait_ms = @min(wait_ms, remaining);
|
||||
} else {
|
||||
wait_ms = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const watched_fd: ?posix.fd_t = if (extra_fds.len > 0) extra_fds[0].fd else ctx.cdp_fd;
|
||||
const watched_ready = try ctx.runtime.dispatchFor(wait_ms, watched_fd);
|
||||
|
||||
if (extra_fds.len > 0) {
|
||||
extra_fds[0].revents = .{};
|
||||
if (watched_ready) {
|
||||
extra_fds[0].revents.pollin = true;
|
||||
}
|
||||
}
|
||||
|
||||
try self.runDueTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
try libcurl.curl_multi_poll(self.multi, extra_fds, timeout_ms, null);
|
||||
}
|
||||
|
||||
@@ -573,6 +641,128 @@ pub const Handles = struct {
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn attachRuntime(self: *Handles, runtime: *Runtime, cdp_fd: ?posix.fd_t) !void {
|
||||
self.detachRuntime();
|
||||
self.runtime_ctx = .{
|
||||
.handles = self,
|
||||
.runtime = runtime,
|
||||
.cdp_fd = cdp_fd,
|
||||
.timer_deadline_ms = null,
|
||||
};
|
||||
|
||||
const ctx = &self.runtime_ctx.?;
|
||||
try libcurl.curl_multi_setopt(self.multi, .socket_function, onCurlSocket);
|
||||
try libcurl.curl_multi_setopt(self.multi, .socket_data, ctx);
|
||||
try libcurl.curl_multi_setopt(self.multi, .timer_function, onCurlTimer);
|
||||
try libcurl.curl_multi_setopt(self.multi, .timer_data, ctx);
|
||||
_ = try self.socketActionTimeout();
|
||||
}
|
||||
|
||||
pub fn detachRuntime(self: *Handles) void {
|
||||
if (self.runtime_ctx) |*ctx| {
|
||||
ctx.runtime.removeByCtx(ctx);
|
||||
}
|
||||
libcurl.curl_multi_setopt(self.multi, .socket_function, @as(?*const libcurl.CurlSocketCallback, null)) catch {};
|
||||
libcurl.curl_multi_setopt(self.multi, .socket_data, @as(?*anyopaque, null)) catch {};
|
||||
libcurl.curl_multi_setopt(self.multi, .timer_function, @as(?*const libcurl.CurlTimerCallback, null)) catch {};
|
||||
libcurl.curl_multi_setopt(self.multi, .timer_data, @as(?*anyopaque, null)) catch {};
|
||||
self.runtime_ctx = null;
|
||||
}
|
||||
|
||||
fn runDueTimer(self: *Handles) !void {
|
||||
const ctx = &(self.runtime_ctx orelse return);
|
||||
const deadline = ctx.timer_deadline_ms orelse return;
|
||||
if (std.time.milliTimestamp() < deadline) {
|
||||
return;
|
||||
}
|
||||
ctx.timer_deadline_ms = null;
|
||||
_ = try self.socketActionTimeout();
|
||||
}
|
||||
|
||||
fn socketAction(self: *Handles, fd: posix.fd_t, events: u32) !c_int {
|
||||
const select_mask = runtimeEventsToCurlSelect(events).toC();
|
||||
var running: c_int = undefined;
|
||||
self.performing = true;
|
||||
defer self.performing = false;
|
||||
try libcurl.curl_multi_socket_action(self.multi, @intCast(fd), select_mask, &running);
|
||||
return running;
|
||||
}
|
||||
|
||||
fn socketActionTimeout(self: *Handles) !c_int {
|
||||
var running: c_int = undefined;
|
||||
self.performing = true;
|
||||
defer self.performing = false;
|
||||
try libcurl.curl_multi_socket_action(self.multi, libcurl.CURL_SOCKET_TIMEOUT, 0, &running);
|
||||
return running;
|
||||
}
|
||||
|
||||
fn runtimeEventsToCurlSelect(events: u32) libcurl.CurlSelectMask {
|
||||
return .{
|
||||
.in = (events & Runtime.READABLE) != 0,
|
||||
.out = (events & Runtime.WRITABLE) != 0,
|
||||
.err = (events & Runtime.ERROR) != 0,
|
||||
};
|
||||
}
|
||||
|
||||
fn curlPollToRuntimeEvents(what: libcurl.CurlPoll) u32 {
|
||||
return switch (what) {
|
||||
.in => Runtime.READABLE | Runtime.ERROR,
|
||||
.out => Runtime.WRITABLE | Runtime.ERROR,
|
||||
.inout => Runtime.READABLE | Runtime.WRITABLE | Runtime.ERROR,
|
||||
.remove => 0,
|
||||
};
|
||||
}
|
||||
|
||||
fn onRuntimeFdEvent(ctx_ptr: *anyopaque, event: RuntimeEvent) anyerror!void {
|
||||
const ctx: *RuntimeContext = @ptrCast(@alignCast(ctx_ptr));
|
||||
_ = try ctx.handles.socketAction(event.fd, event.events);
|
||||
}
|
||||
|
||||
fn onCurlSocket(
|
||||
easy: ?*libcurl.Curl,
|
||||
s: libcurl.CurlSocket,
|
||||
what_raw: c_int,
|
||||
userp: ?*anyopaque,
|
||||
socketp: ?*anyopaque,
|
||||
) callconv(.c) c_int {
|
||||
_ = easy;
|
||||
_ = socketp;
|
||||
const ctx = userp orelse return 0;
|
||||
const runtime_ctx: *RuntimeContext = @ptrCast(@alignCast(ctx));
|
||||
|
||||
const what = std.meta.intToEnum(libcurl.CurlPoll, what_raw) catch return 0;
|
||||
if (what == .remove) {
|
||||
runtime_ctx.runtime.remove(@intCast(s));
|
||||
return 0;
|
||||
}
|
||||
|
||||
runtime_ctx.runtime.add(
|
||||
@intCast(s),
|
||||
curlPollToRuntimeEvents(what),
|
||||
runtime_ctx,
|
||||
onRuntimeFdEvent,
|
||||
) catch {};
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn onCurlTimer(
|
||||
multi: ?*libcurl.CurlM,
|
||||
timeout_ms: c_long,
|
||||
userp: ?*anyopaque,
|
||||
) callconv(.c) c_int {
|
||||
_ = multi;
|
||||
const ctx = userp orelse return 0;
|
||||
const runtime_ctx: *RuntimeContext = @ptrCast(@alignCast(ctx));
|
||||
|
||||
if (timeout_ms < 0) {
|
||||
runtime_ctx.timer_deadline_ms = null;
|
||||
return 0;
|
||||
}
|
||||
|
||||
runtime_ctx.timer_deadline_ms = std.time.milliTimestamp() + @as(i64, @intCast(timeout_ms));
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: on BSD / Linux, we could just read the PEM file directly.
|
||||
@@ -1356,6 +1546,148 @@ pub const WsConnection = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const RuntimeEvent = struct {
|
||||
fd: posix.fd_t,
|
||||
events: u32,
|
||||
};
|
||||
|
||||
pub const Runtime = PollRuntime;
|
||||
|
||||
var runtime_active = std.atomic.Value(bool).init(false);
|
||||
|
||||
const PollRuntime = struct {
|
||||
pub const READABLE: u32 = @intCast(posix.POLL.IN);
|
||||
pub const WRITABLE: u32 = @intCast(posix.POLL.OUT);
|
||||
pub const ERROR: u32 = @intCast(posix.POLL.ERR | posix.POLL.HUP | posix.POLL.NVAL);
|
||||
|
||||
allocator: Allocator,
|
||||
watchers: std.AutoHashMapUnmanaged(posix.fd_t, Watcher) = .empty,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub const EventCallback = *const fn (ctx: *anyopaque, event: RuntimeEvent) anyerror!void;
|
||||
|
||||
const Watcher = struct {
|
||||
events: u32,
|
||||
ctx: *anyopaque,
|
||||
cb: EventCallback,
|
||||
};
|
||||
|
||||
pub fn init(allocator: Allocator) !Self {
|
||||
if (runtime_active.swap(true, .acq_rel)) {
|
||||
return error.RuntimeAlreadyActive;
|
||||
}
|
||||
errdefer _ = runtime_active.swap(false, .acq_rel);
|
||||
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.watchers.deinit(self.allocator);
|
||||
_ = runtime_active.swap(false, .acq_rel);
|
||||
}
|
||||
|
||||
pub fn add(self: *Self, fd: posix.fd_t, events: u32, ctx: *anyopaque, cb: EventCallback) !void {
|
||||
const gop = try self.watchers.getOrPut(self.allocator, fd);
|
||||
gop.value_ptr.* = .{ .events = events, .ctx = ctx, .cb = cb };
|
||||
}
|
||||
|
||||
pub fn remove(self: *Self, fd: posix.fd_t) void {
|
||||
_ = self.watchers.remove(fd);
|
||||
}
|
||||
|
||||
pub fn removeByCtx(self: *Self, ctx: *anyopaque) void {
|
||||
while (true) {
|
||||
var found: ?posix.fd_t = null;
|
||||
var it = self.watchers.iterator();
|
||||
while (it.next()) |entry| {
|
||||
if (entry.value_ptr.ctx == ctx) {
|
||||
found = entry.key_ptr.*;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const fd = found orelse break;
|
||||
self.remove(fd);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(self: *Self, stop_ctx: *anyopaque, should_stop: *const fn (ctx: *anyopaque) bool) !void {
|
||||
while (!should_stop(stop_ctx)) {
|
||||
_ = try self.dispatch(200);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispatch(self: *Self, timeout_ms: i32) !usize {
|
||||
var triggered: usize = 0;
|
||||
_ = try self.dispatchPoll(timeout_ms, null, &triggered);
|
||||
return triggered;
|
||||
}
|
||||
|
||||
pub fn dispatchFor(self: *Self, timeout_ms: i32, watched_fd: ?posix.fd_t) !bool {
|
||||
var triggered: usize = 0;
|
||||
return self.dispatchPoll(timeout_ms, watched_fd, &triggered);
|
||||
}
|
||||
|
||||
fn dispatchPoll(self: *Self, timeout_ms: i32, watched_fd: ?posix.fd_t, triggered: *usize) !bool {
|
||||
var pollfds: std.ArrayList(posix.pollfd) = .empty;
|
||||
defer pollfds.deinit(self.allocator);
|
||||
|
||||
var it = self.watchers.iterator();
|
||||
while (it.next()) |entry| {
|
||||
try pollfds.append(self.allocator, .{
|
||||
.fd = entry.key_ptr.*,
|
||||
.events = @intCast(entry.value_ptr.events),
|
||||
.revents = 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (watched_fd) |wfd| {
|
||||
if (!self.watchers.contains(wfd)) {
|
||||
try pollfds.append(self.allocator, .{
|
||||
.fd = wfd,
|
||||
.events = @intCast(Self.READABLE | Self.ERROR),
|
||||
.revents = 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (pollfds.items.len == 0) {
|
||||
if (timeout_ms > 0) {
|
||||
std.Thread.sleep(@as(u64, @intCast(timeout_ms)) * std.time.ns_per_ms);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_ = try posix.poll(pollfds.items, timeout_ms);
|
||||
|
||||
var watched_ready = false;
|
||||
for (pollfds.items) |pfd| {
|
||||
if (pfd.revents == 0) continue;
|
||||
triggered.* += 1;
|
||||
|
||||
const revents_u32: u32 = @intCast(pfd.revents);
|
||||
if (watched_fd) |wfd| {
|
||||
if (pfd.fd == wfd) {
|
||||
watched_ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
const watcher = self.watchers.get(pfd.fd) orelse continue;
|
||||
watcher.cb(watcher.ctx, .{
|
||||
.fd = pfd.fd,
|
||||
.events = revents_u32,
|
||||
}) catch |err| {
|
||||
log.err(.app, "runtime callback", .{ .err = err, .fd = pfd.fd });
|
||||
self.remove(pfd.fd);
|
||||
};
|
||||
}
|
||||
|
||||
return watched_ready;
|
||||
}
|
||||
};
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
test "mask" {
|
||||
|
||||
@@ -121,26 +121,52 @@ pub fn run(self: *Server, address: net.Address, timeout_ms: u32) !void {
|
||||
try posix.listen(listener, self.app.config.maxPendingConnections());
|
||||
|
||||
log.info(.app, "server running", .{ .address = address });
|
||||
while (!self.shutdown.load(.acquire)) {
|
||||
const socket = posix.accept(listener, null, null, posix.SOCK.NONBLOCK) catch |err| {
|
||||
switch (err) {
|
||||
error.SocketNotListening, error.ConnectionAborted => {
|
||||
log.info(.app, "server stopped", .{});
|
||||
break;
|
||||
},
|
||||
error.WouldBlock => {
|
||||
std.Thread.sleep(10 * std.time.ns_per_ms);
|
||||
continue;
|
||||
},
|
||||
else => {
|
||||
log.err(.app, "CDP accept", .{ .err = err });
|
||||
std.Thread.sleep(std.time.ns_per_s);
|
||||
continue;
|
||||
},
|
||||
}
|
||||
|
||||
var runtime = try Net.Runtime.init(self.allocator);
|
||||
defer runtime.deinit();
|
||||
|
||||
var accept_ctx: AcceptCtx = .{
|
||||
.server = self,
|
||||
.timeout_ms = timeout_ms,
|
||||
};
|
||||
|
||||
try runtime.add(listener, Net.Runtime.READABLE, &accept_ctx, onListenerEvent);
|
||||
defer runtime.remove(listener);
|
||||
|
||||
runtime.run(self, shouldStopRuntime) catch |err| {
|
||||
return err;
|
||||
};
|
||||
|
||||
log.info(.app, "server stopped", .{});
|
||||
}
|
||||
|
||||
const AcceptCtx = struct {
|
||||
server: *Server,
|
||||
timeout_ms: u32,
|
||||
};
|
||||
|
||||
fn shouldStopRuntime(ctx: *anyopaque) bool {
|
||||
const self: *Server = @ptrCast(@alignCast(ctx));
|
||||
return self.shutdown.load(.acquire);
|
||||
}
|
||||
|
||||
fn onListenerEvent(ctx: *anyopaque, event: Net.RuntimeEvent) !void {
|
||||
_ = event;
|
||||
const accept_ctx: *AcceptCtx = @ptrCast(@alignCast(ctx));
|
||||
const self = accept_ctx.server;
|
||||
const listener = self.listener orelse return;
|
||||
|
||||
while (true) {
|
||||
const socket = posix.accept(listener, null, null, posix.SOCK.NONBLOCK) catch |err| switch (err) {
|
||||
error.WouldBlock => return,
|
||||
error.SocketNotListening, error.ConnectionAborted => return,
|
||||
else => {
|
||||
log.err(.app, "CDP accept", .{ .err = err });
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
self.spawnWorker(socket, timeout_ms) catch |err| {
|
||||
self.spawnWorker(socket, accept_ctx.timeout_ms) catch |err| {
|
||||
log.err(.app, "CDP spawn", .{ .err = err });
|
||||
posix.close(socket);
|
||||
};
|
||||
@@ -296,6 +322,10 @@ pub const Client = struct {
|
||||
}
|
||||
|
||||
fn stop(self: *Client) void {
|
||||
switch (self.mode) {
|
||||
.http => {},
|
||||
.cdp => |*cdp| cdp.browser.env.terminate(),
|
||||
}
|
||||
self.ws.shutdown();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const URL = @import("browser/URL.zig");
|
||||
|
||||
const TestHTTPServer = @This();
|
||||
|
||||
@@ -97,7 +98,10 @@ fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !voi
|
||||
}
|
||||
|
||||
pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void {
|
||||
var file = std.fs.cwd().openFile(file_path, .{}) catch |err| switch (err) {
|
||||
var url_buf: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&url_buf);
|
||||
const unescaped_file_path = try URL.unescape(fba.allocator(), file_path);
|
||||
var file = std.fs.cwd().openFile(unescaped_file_path, .{}) catch |err| switch (err) {
|
||||
error.FileNotFound => return req.respond("server error", .{ .status = .not_found }),
|
||||
else => return err,
|
||||
};
|
||||
|
||||
@@ -377,12 +377,17 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
|
||||
const page = self.page;
|
||||
var was_handled = false;
|
||||
|
||||
defer if (was_handled) {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
ls.local.runMicrotasks();
|
||||
};
|
||||
// Create a single scope for all event handlers in this dispatch.
|
||||
// This ensures function handles passed to queueMicrotask remain valid
|
||||
// throughout the entire dispatch, preventing crashes when microtasks run.
|
||||
var ls: js.Local.Scope = undefined;
|
||||
page.js.localScope(&ls);
|
||||
defer {
|
||||
if (was_handled) {
|
||||
ls.local.runMicrotasks();
|
||||
}
|
||||
ls.deinit();
|
||||
}
|
||||
|
||||
const activation_state = ActivationState.create(event, target, page);
|
||||
|
||||
@@ -461,7 +466,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
|
||||
.event_target = @intFromPtr(current_target),
|
||||
.type_string = event._type_string,
|
||||
})) |list| {
|
||||
try self.dispatchPhase(list, current_target, event, &was_handled, comptime .init(true, opts));
|
||||
try self.dispatchPhase(list, current_target, event, &was_handled, &ls.local, comptime .init(true, opts));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,10 +481,6 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
|
||||
was_handled = true;
|
||||
event._current_target = target_et;
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
self.page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
try ls.toLocal(inline_handler).callWithThis(void, target_et, .{event});
|
||||
|
||||
if (event._stop_propagation) {
|
||||
@@ -495,7 +496,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
|
||||
.type_string = event._type_string,
|
||||
.event_target = @intFromPtr(target_et),
|
||||
})) |list| {
|
||||
try self.dispatchPhase(list, target_et, event, &was_handled, comptime .init(null, opts));
|
||||
try self.dispatchPhase(list, target_et, event, &was_handled, &ls.local, comptime .init(null, opts));
|
||||
if (event._stop_propagation) {
|
||||
return;
|
||||
}
|
||||
@@ -512,7 +513,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
|
||||
.type_string = event._type_string,
|
||||
.event_target = @intFromPtr(current_target),
|
||||
})) |list| {
|
||||
try self.dispatchPhase(list, current_target, event, &was_handled, comptime .init(false, opts));
|
||||
try self.dispatchPhase(list, current_target, event, &was_handled, &ls.local, comptime .init(false, opts));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -530,7 +531,7 @@ const DispatchPhaseOpts = struct {
|
||||
}
|
||||
};
|
||||
|
||||
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime opts: DispatchPhaseOpts) !void {
|
||||
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, local: *const js.Local, comptime opts: DispatchPhaseOpts) !void {
|
||||
const page = self.page;
|
||||
|
||||
// Track dispatch depth for deferred removal
|
||||
@@ -607,18 +608,14 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
||||
event._target = getAdjustedTarget(original_target, current_target);
|
||||
}
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
switch (listener.function) {
|
||||
.value => |value| try ls.toLocal(value).callWithThis(void, current_target, .{event}),
|
||||
.value => |value| try local.toLocal(value).callWithThis(void, current_target, .{event}),
|
||||
.string => |string| {
|
||||
const str = try page.call_arena.dupeZ(u8, string.str());
|
||||
try ls.local.eval(str, null);
|
||||
try local.eval(str, null);
|
||||
},
|
||||
.object => |obj_global| {
|
||||
const obj = ls.toLocal(obj_global);
|
||||
const obj = local.toLocal(obj_global);
|
||||
if (try obj.getFunction("handleEvent")) |handleEvent| {
|
||||
try handleEvent.callWithThis(void, obj, .{event});
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ version: usize = 0,
|
||||
// ScriptManager, so all scripts just count as 1 pending load.
|
||||
_pending_loads: u32,
|
||||
|
||||
_parent_notified: if (IS_DEBUG) bool else void = if (IS_DEBUG) false else {},
|
||||
_parent_notified: bool = false,
|
||||
|
||||
_type: enum { root, frame }, // only used for logs right now
|
||||
_req_id: u32 = 0,
|
||||
@@ -346,7 +346,10 @@ pub fn deinit(self: *Page) void {
|
||||
session.browser.env.destroyContext(self.js);
|
||||
|
||||
self._script_manager.shutdown = true;
|
||||
session.browser.http_client.abort();
|
||||
if (self.parent == null) {
|
||||
// only the root frame needs to abort this. It's more efficient this way
|
||||
session.browser.http_client.abort();
|
||||
}
|
||||
self._script_manager.deinit();
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
@@ -460,8 +463,12 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
||||
// It's important to force a reset during the following navigation.
|
||||
self._parse_state = .complete;
|
||||
|
||||
// We do not processHTMLDoc here as we know we don't have any scripts
|
||||
// This assumption may be false when CDP Page.addScriptToEvaluateOnNewDocument is implemented
|
||||
{
|
||||
const parse_arena = try self.getArena(.{ .debug = "about:blank parse" });
|
||||
defer self.releaseArena(parse_arena);
|
||||
var parser = Parser.init(parse_arena, self.document.asNode(), self);
|
||||
parser.parse("<html><head></head><body></body></html>");
|
||||
}
|
||||
self.documentIsComplete();
|
||||
|
||||
session.notification.dispatch(.page_navigate, &.{
|
||||
@@ -707,17 +714,18 @@ pub fn documentIsComplete(self: *Page) void {
|
||||
log.err(.page, "document is complete", .{ .err = err, .type = self._type, .url = self.url });
|
||||
};
|
||||
|
||||
if (IS_DEBUG) {
|
||||
std.debug.assert(self._navigated_options != null);
|
||||
if (self._navigated_options) |no| {
|
||||
// _navigated_options will be null in special short-circuit cases, like
|
||||
// "navigating" to about:blank, in which case this notification has
|
||||
// already been sent
|
||||
self._session.notification.dispatch(.page_navigated, &.{
|
||||
.frame_id = self._frame_id,
|
||||
.req_id = self._req_id,
|
||||
.opts = no,
|
||||
.url = self.url,
|
||||
.timestamp = timestamp(.monotonic),
|
||||
});
|
||||
}
|
||||
|
||||
self._session.notification.dispatch(.page_navigated, &.{
|
||||
.frame_id = self._frame_id,
|
||||
.req_id = self._req_id,
|
||||
.opts = self._navigated_options.?,
|
||||
.url = self.url,
|
||||
.timestamp = timestamp(.monotonic),
|
||||
});
|
||||
}
|
||||
|
||||
fn _documentIsComplete(self: *Page) !void {
|
||||
@@ -750,11 +758,15 @@ fn _documentIsComplete(self: *Page) !void {
|
||||
}
|
||||
|
||||
fn notifyParentLoadComplete(self: *Page) void {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(self._parent_notified == false);
|
||||
self._parent_notified = true;
|
||||
if (self._parent_notified == true) {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(false);
|
||||
}
|
||||
// shouldn't happen, don't want to crash a release build over it
|
||||
return;
|
||||
}
|
||||
|
||||
self._parent_notified = true;
|
||||
if (self.parent) |p| {
|
||||
p.iframeCompletedLoading(self.iframe.?);
|
||||
}
|
||||
@@ -796,7 +808,12 @@ fn pageDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||
} orelse .unknown;
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.page, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len, .type = self._type, .url = self.url });
|
||||
log.debug(.page, "navigate first chunk", .{
|
||||
.content_type = mime.content_type,
|
||||
.len = data.len,
|
||||
.type = self._type,
|
||||
.url = self.url,
|
||||
});
|
||||
}
|
||||
|
||||
switch (mime.content_type) {
|
||||
@@ -850,7 +867,11 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
|
||||
try self._session.navigation.commitNavigation(self);
|
||||
|
||||
defer if (comptime IS_DEBUG) {
|
||||
log.debug(.page, "page.load.complete", .{ .url = self.url, .type = self._type });
|
||||
log.debug(.page, "page load complete", .{
|
||||
.url = self.url,
|
||||
.type = self._type,
|
||||
.state = std.meta.activeTag(self._parse_state),
|
||||
});
|
||||
};
|
||||
|
||||
const parse_arena = try self.getArena(.{ .debug = "Page.parse" });
|
||||
@@ -962,29 +983,49 @@ pub fn iframeAddedCallback(self: *Page, iframe: *Element.Html.IFrame) !void {
|
||||
}
|
||||
|
||||
iframe._executed = true;
|
||||
|
||||
const session = self._session;
|
||||
const frame_id = session.nextFrameId();
|
||||
|
||||
// A frame can be re-navigated by setting the src.
|
||||
const existing_window = iframe._content_window;
|
||||
|
||||
const page_frame = try self.arena.create(Page);
|
||||
const frame_id = blk: {
|
||||
if (existing_window) |w| {
|
||||
const existing_frame_id = w._page._frame_id;
|
||||
session.browser.http_client.abortFrame(existing_frame_id);
|
||||
break :blk existing_frame_id;
|
||||
}
|
||||
break :blk session.nextFrameId();
|
||||
};
|
||||
|
||||
try Page.init(page_frame, frame_id, session, self);
|
||||
errdefer page_frame.deinit();
|
||||
|
||||
self._pending_loads += 1;
|
||||
page_frame.iframe = iframe;
|
||||
iframe._content_window = page_frame.window;
|
||||
errdefer iframe._content_window = null;
|
||||
|
||||
self._session.notification.dispatch(.page_frame_created, &.{
|
||||
.frame_id = frame_id,
|
||||
.parent_id = self._frame_id,
|
||||
.timestamp = timestamp(.monotonic),
|
||||
});
|
||||
const url = blk: {
|
||||
if (std.mem.eql(u8, src, "about:blank")) {
|
||||
break :blk "about:blank"; // navigate will handle this special case
|
||||
}
|
||||
break :blk try URL.resolve(
|
||||
self.call_arena, // ok to use, page.navigate dupes this
|
||||
self.base(),
|
||||
src,
|
||||
.{ .encode = true },
|
||||
);
|
||||
};
|
||||
|
||||
// navigate will dupe the url
|
||||
const url = try URL.resolve(
|
||||
self.call_arena,
|
||||
self.base(),
|
||||
src,
|
||||
.{ .encode = true },
|
||||
);
|
||||
if (existing_window == null) {
|
||||
// on first load, dispatch frame_created evnet
|
||||
self._session.notification.dispatch(.page_frame_created, &.{
|
||||
.frame_id = frame_id,
|
||||
.parent_id = self._frame_id,
|
||||
.timestamp = timestamp(.monotonic),
|
||||
});
|
||||
}
|
||||
|
||||
page_frame.navigate(url, .{ .reason = .initialFrameNavigation }) catch |err| {
|
||||
log.warn(.page, "iframe navigate failure", .{ .url = url, .err = err });
|
||||
@@ -994,6 +1035,25 @@ pub fn iframeAddedCallback(self: *Page, iframe: *Element.Html.IFrame) !void {
|
||||
return error.IFrameLoadError;
|
||||
};
|
||||
|
||||
if (existing_window) |w| {
|
||||
const existing_page = w._page;
|
||||
if (existing_page._parent_notified == false) {
|
||||
self._pending_loads -= 1;
|
||||
}
|
||||
|
||||
for (self.frames.items, 0..) |p, i| {
|
||||
if (p == existing_page) {
|
||||
self.frames.items[i] = page_frame;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
lp.assert(false, "Existing frame not found", .{ .len = self.frames.items.len });
|
||||
}
|
||||
|
||||
existing_page.deinit();
|
||||
return;
|
||||
}
|
||||
|
||||
// window[N] is based on document order. For now we'll just append the frame
|
||||
// at the end of our list and set frames_sorted == false. window.getFrame
|
||||
// will check this flag to decide if it needs to sort the frames or not.
|
||||
@@ -1825,7 +1885,7 @@ pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const
|
||||
Element.Html.Track,
|
||||
namespace,
|
||||
attribute_iterator,
|
||||
.{ ._proto = undefined },
|
||||
.{ ._proto = undefined, ._kind = comptime .wrap("subtitles"), ._ready_state = .none },
|
||||
),
|
||||
else => {},
|
||||
},
|
||||
@@ -1942,10 +2002,10 @@ pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const
|
||||
.{ ._proto = undefined, ._tag_name = String.init(undefined, "article", .{}) catch unreachable, ._tag = .article },
|
||||
),
|
||||
asUint("details") => return self.createHtmlElementT(
|
||||
Element.Html.Generic,
|
||||
Element.Html.Details,
|
||||
namespace,
|
||||
attribute_iterator,
|
||||
.{ ._proto = undefined, ._tag_name = String.init(undefined, "details", .{}) catch unreachable, ._tag = .details },
|
||||
.{ ._proto = undefined },
|
||||
),
|
||||
asUint("summary") => return self.createHtmlElementT(
|
||||
Element.Html.Generic,
|
||||
@@ -2469,7 +2529,7 @@ pub fn insertNodeRelative(self: *Page, parent: *Node, child: *Node, relative: In
|
||||
pub fn _insertNodeRelative(self: *Page, comptime from_parser: bool, parent: *Node, child: *Node, relative: InsertNodeRelative, opts: InsertNodeOpts) !void {
|
||||
// caller should have made sure this was the case
|
||||
|
||||
lp.assert(child._parent == null, "Page.insertNodeRelative parent", .{ .url = self.url });
|
||||
lp.assert(child._parent == null, "Page.insertNodeRelative parent", .{});
|
||||
|
||||
const children = blk: {
|
||||
// expand parent._children so that it can take another child
|
||||
|
||||
@@ -634,6 +634,8 @@ pub const Script = struct {
|
||||
debug_transfer_notified_fail: bool = false,
|
||||
debug_transfer_redirecting: bool = false,
|
||||
debug_transfer_intercept_state: u8 = 0,
|
||||
debug_transfer_auth_challenge: bool = false,
|
||||
debug_transfer_easy_id: usize = 0,
|
||||
|
||||
const Kind = enum {
|
||||
module,
|
||||
@@ -711,6 +713,8 @@ pub const Script = struct {
|
||||
.a5 = self.debug_transfer_notified_fail,
|
||||
.a6 = self.debug_transfer_redirecting,
|
||||
.a7 = self.debug_transfer_intercept_state,
|
||||
.a8 = self.debug_transfer_auth_challenge,
|
||||
.a9 = self.debug_transfer_easy_id,
|
||||
.b1 = transfer.id,
|
||||
.b2 = transfer._tries,
|
||||
.b3 = transfer.aborted,
|
||||
@@ -718,6 +722,8 @@ pub const Script = struct {
|
||||
.b5 = transfer._notified_fail,
|
||||
.b6 = transfer._redirecting,
|
||||
.b7 = @intFromEnum(transfer._intercept_state),
|
||||
.b8 = transfer._auth_challenge != null,
|
||||
.b9 = if (transfer._conn) |c| @intFromPtr(c.easy) else 0,
|
||||
});
|
||||
self.header_callback_called = true;
|
||||
self.debug_transfer_id = transfer.id;
|
||||
@@ -727,6 +733,8 @@ pub const Script = struct {
|
||||
self.debug_transfer_notified_fail = transfer._notified_fail;
|
||||
self.debug_transfer_redirecting = transfer._redirecting;
|
||||
self.debug_transfer_intercept_state = @intFromEnum(transfer._intercept_state);
|
||||
self.debug_transfer_auth_challenge = transfer._auth_challenge != null;
|
||||
self.debug_transfer_easy_id = if (transfer._conn) |c| @intFromPtr(c.easy) else 0;
|
||||
}
|
||||
|
||||
lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity });
|
||||
|
||||
@@ -961,6 +961,10 @@ test "URL: ensureEncoded" {
|
||||
.url = "https://example.com/path?value=100% done",
|
||||
.expected = "https://example.com/path?value=100%25%20done",
|
||||
},
|
||||
.{
|
||||
.url = "about:blank",
|
||||
.expected = "about:blank",
|
||||
},
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
|
||||
@@ -480,10 +480,11 @@ fn consumeName(self: *Tokenizer) []const u8 {
|
||||
self.consumeEscape();
|
||||
},
|
||||
0x0 => self.advance(1),
|
||||
'\x80'...'\xBF', '\xC0'...'\xEF', '\xF0'...'\xFF' => {
|
||||
// This byte *is* part of a multi-byte code point,
|
||||
// we’ll end up copying the whole code point before this loop does something else.
|
||||
self.advance(1);
|
||||
'\x80'...'\xFF' => {
|
||||
// Non-ASCII: advance over the complete UTF-8 code point in one step.
|
||||
// Using consumeChar() instead of advance(1) ensures we never land on
|
||||
// a continuation byte, which advance() asserts against.
|
||||
self.consumeChar();
|
||||
},
|
||||
else => {
|
||||
if (self.hasNonAsciiAt(0)) {
|
||||
|
||||
@@ -60,6 +60,11 @@ fn initWithContext(self: *Caller, ctx: *Context, v8_context: *const v8.Context)
|
||||
ctx.local = &self.local;
|
||||
}
|
||||
|
||||
pub fn initFromHandle(self: *Caller, handle: ?*const v8.FunctionCallbackInfo) void {
|
||||
const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
||||
self.init(isolate);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Caller) void {
|
||||
const ctx = self.local.ctx;
|
||||
const call_depth = ctx.call_depth - 1;
|
||||
@@ -441,6 +446,11 @@ pub const FunctionCallbackInfo = struct {
|
||||
return .{ .local = local, .handle = v8.v8__FunctionCallbackInfo__INDEX(self.handle, @intCast(index)).? };
|
||||
}
|
||||
|
||||
pub fn getData(self: FunctionCallbackInfo) ?*anyopaque {
|
||||
const data = v8.v8__FunctionCallbackInfo__Data(self.handle) orelse return null;
|
||||
return v8.v8__External__Value(@ptrCast(data));
|
||||
}
|
||||
|
||||
pub fn getThis(self: FunctionCallbackInfo) *const v8.Object {
|
||||
return v8.v8__FunctionCallbackInfo__This(self.handle).?;
|
||||
}
|
||||
@@ -499,6 +509,7 @@ pub const Function = struct {
|
||||
as_typed_array: bool = false,
|
||||
null_as_undefined: bool = false,
|
||||
cache: ?Caching = null,
|
||||
embedded_receiver: bool = false,
|
||||
|
||||
// We support two ways to cache a value directly into a v8::Object. The
|
||||
// difference between the two is like the difference between a Map
|
||||
@@ -569,6 +580,9 @@ pub const Function = struct {
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
if (comptime opts.static) {
|
||||
args = try getArgs(F, 0, local, info);
|
||||
} else if (comptime opts.embedded_receiver) {
|
||||
args = try getArgs(F, 1, local, info);
|
||||
@field(args, "0") = @ptrCast(@alignCast(info.getData() orelse unreachable));
|
||||
} else {
|
||||
args = try getArgs(F, 1, local, info);
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
|
||||
@@ -786,9 +786,16 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
|
||||
entry.module_promise = try module_resolver.promise().persist();
|
||||
} else {
|
||||
// the module was loaded, but not evaluated, we _have_ to evaluate it now
|
||||
if (status == .kUninstantiated) {
|
||||
if (try mod.instantiate(resolveModuleCallback) == false) {
|
||||
_ = resolver.reject("module instantiation", local.newString("Module instantiation failed"));
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
const evaluated = mod.evaluate() catch {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(status == .kErrored);
|
||||
std.debug.assert(mod.getStatus() == .kErrored);
|
||||
}
|
||||
_ = resolver.reject("module evaluation", local.newString("Module evaluation failed"));
|
||||
return promise;
|
||||
@@ -868,13 +875,12 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul
|
||||
|
||||
const then_callback = newFunctionWithData(local, struct {
|
||||
pub fn callback(callback_handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(callback_handle).?;
|
||||
var c: Caller = undefined;
|
||||
c.init(isolate);
|
||||
c.initFromHandle(callback_handle);
|
||||
defer c.deinit();
|
||||
|
||||
const info_data = v8.v8__FunctionCallbackInfo__Data(callback_handle).?;
|
||||
const s: *DynamicModuleResolveState = @ptrCast(@alignCast(v8.v8__External__Value(@ptrCast(info_data))));
|
||||
const info = Caller.FunctionCallbackInfo{ .handle = callback_handle.? };
|
||||
const s: *DynamicModuleResolveState = @ptrCast(@alignCast(info.getData() orelse return));
|
||||
|
||||
if (s.context_id != c.local.ctx.id) {
|
||||
// The microtask is tied to the isolate, not the context
|
||||
@@ -893,17 +899,15 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul
|
||||
|
||||
const catch_callback = newFunctionWithData(local, struct {
|
||||
pub fn callback(callback_handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(callback_handle).?;
|
||||
var c: Caller = undefined;
|
||||
c.init(isolate);
|
||||
c.initFromHandle(callback_handle);
|
||||
defer c.deinit();
|
||||
|
||||
const info_data = v8.v8__FunctionCallbackInfo__Data(callback_handle).?;
|
||||
const s: *DynamicModuleResolveState = @ptrCast(@alignCast(v8.v8__External__Value(@ptrCast(info_data))));
|
||||
const info = Caller.FunctionCallbackInfo{ .handle = callback_handle.? };
|
||||
const s: *DynamicModuleResolveState = @ptrCast(@alignCast(info.getData() orelse return));
|
||||
|
||||
const l = &c.local;
|
||||
const ctx = l.ctx;
|
||||
if (s.context_id != ctx.id) {
|
||||
if (s.context_id != l.ctx.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1007,6 +1011,13 @@ fn enqueueMicrotask(self: *Context, callback: anytype) void {
|
||||
}.run, self);
|
||||
}
|
||||
|
||||
// There's an assumption here: the js.Function will be alive when microtasks are
|
||||
// run. If we're Env.runMicrotasks in all the places that we're supposed to, then
|
||||
// this should be safe (I think). In whatever HandleScope a microtask is enqueued,
|
||||
// PerformCheckpoint should be run. So the v8::Local<v8::Function> should remain
|
||||
// valid. If we have problems with this, a simple solution is to provide a Zig
|
||||
// wrapper for these callbacks which references a js.Function.Temp, on callback
|
||||
// it executes the function and then releases the global.
|
||||
pub fn queueMicrotaskFunc(self: *Context, cb: js.Function) void {
|
||||
// Use context-specific microtask queue instead of isolate queue
|
||||
v8.v8__MicrotaskQueue__EnqueueMicrotaskFunc(self.microtask_queue, self.isolate.handle, cb.handle);
|
||||
|
||||
@@ -470,6 +470,10 @@ pub fn dumpMemoryStats(self: *Env) void {
|
||||
, .{ stats.total_heap_size, stats.total_heap_size_executable, stats.total_physical_size, stats.total_available_size, stats.used_heap_size, stats.heap_size_limit, stats.malloced_memory, stats.external_memory, stats.peak_malloced_memory, stats.number_of_native_contexts, stats.number_of_detached_contexts, stats.total_global_handles_size, stats.used_global_handles_size, stats.does_zap_garbage });
|
||||
}
|
||||
|
||||
pub fn terminate(self: *const Env) void {
|
||||
v8.v8__Isolate__TerminateExecution(self.isolate.handle);
|
||||
}
|
||||
|
||||
fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
|
||||
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
|
||||
const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
|
||||
|
||||
@@ -82,6 +82,20 @@ pub fn createTypedArray(self: *const Local, comptime array_type: js.ArrayType, s
|
||||
return .init(self, size);
|
||||
}
|
||||
|
||||
pub fn newCallback(
|
||||
self: *const Local,
|
||||
callback: anytype,
|
||||
data: anytype,
|
||||
) js.Function {
|
||||
const external = self.isolate.createExternal(data);
|
||||
const handle = v8.v8__Function__New__DEFAULT2(self.handle, struct {
|
||||
fn wrap(info_handle: ?*const js.v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
Caller.Function.call(@TypeOf(data), info_handle.?, callback, .{ .embedded_receiver = true });
|
||||
}
|
||||
}.wrap, @ptrCast(external)).?;
|
||||
return .{ .local = self, .handle = handle };
|
||||
}
|
||||
|
||||
pub fn runMacrotasks(self: *const Local) void {
|
||||
const env = self.ctx.env;
|
||||
env.pumpMessageLoop();
|
||||
|
||||
@@ -767,6 +767,7 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/element/html/Custom.zig"),
|
||||
@import("../webapi/element/html/Data.zig"),
|
||||
@import("../webapi/element/html/DataList.zig"),
|
||||
@import("../webapi/element/html/Details.zig"),
|
||||
@import("../webapi/element/html/Dialog.zig"),
|
||||
@import("../webapi/element/html/Directory.zig"),
|
||||
@import("../webapi/element/html/DList.zig"),
|
||||
@@ -826,6 +827,8 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/element/svg/Generic.zig"),
|
||||
@import("../webapi/encoding/TextDecoder.zig"),
|
||||
@import("../webapi/encoding/TextEncoder.zig"),
|
||||
@import("../webapi/encoding/TextEncoderStream.zig"),
|
||||
@import("../webapi/encoding/TextDecoderStream.zig"),
|
||||
@import("../webapi/Event.zig"),
|
||||
@import("../webapi/event/CompositionEvent.zig"),
|
||||
@import("../webapi/event/CustomEvent.zig"),
|
||||
@@ -862,6 +865,10 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/streams/ReadableStream.zig"),
|
||||
@import("../webapi/streams/ReadableStreamDefaultReader.zig"),
|
||||
@import("../webapi/streams/ReadableStreamDefaultController.zig"),
|
||||
@import("../webapi/streams/WritableStream.zig"),
|
||||
@import("../webapi/streams/WritableStreamDefaultWriter.zig"),
|
||||
@import("../webapi/streams/WritableStreamDefaultController.zig"),
|
||||
@import("../webapi/streams/TransformStream.zig"),
|
||||
@import("../webapi/Node.zig"),
|
||||
@import("../webapi/storage/storage.zig"),
|
||||
@import("../webapi/URL.zig"),
|
||||
|
||||
@@ -256,3 +256,22 @@
|
||||
testing.expectTrue(!html.includes('opacity:0'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CSSStyleDeclaration_non_ascii_custom_property">
|
||||
{
|
||||
// Regression test: accessing element.style must not crash when the inline
|
||||
// style attribute contains CSS custom properties with non-ASCII (UTF-8
|
||||
// multibyte) names, such as French accented characters.
|
||||
// The CSS Tokenizer's consumeName() must advance over whole UTF-8 sequences
|
||||
// rather than byte-by-byte to avoid landing on a continuation byte.
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('style',
|
||||
'--color-store-bulles-\u00e9t\u00e9-fg: #6a818f;' +
|
||||
'--color-store-soir\u00e9es-odl-fg: #56b3b3;' +
|
||||
'color: red;'
|
||||
);
|
||||
|
||||
// Must not crash, and ASCII properties that follow non-ASCII ones must be readable.
|
||||
testing.expectEqual('red', div.style.getPropertyValue('color'));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -53,3 +53,22 @@
|
||||
testing.expectEqual('NO-CONSTRUCTOR-ELEMENT', el.tagName);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id=clone_container></div>
|
||||
|
||||
<script id=clone>
|
||||
{
|
||||
let calls = 0;
|
||||
class MyCloneElementA extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
calls += 1;
|
||||
$('#clone_container').appendChild(this);
|
||||
}
|
||||
}
|
||||
customElements.define('my-clone_element_a', MyCloneElementA);
|
||||
const original = document.createElement('my-clone_element_a');
|
||||
$('#clone_container').cloneNode(true);
|
||||
testing.expectEqual(2, calls);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -111,3 +111,15 @@
|
||||
const containerDataTest = document.querySelector('#container [data-test]');
|
||||
testing.expectEqual('First', containerDataTest.innerText);
|
||||
</script>
|
||||
|
||||
<link rel="preload" as="image" imagesrcset="url1.png 1x, url2.png 2x" id="preload-link">
|
||||
|
||||
<script id="commaInAttrValue">
|
||||
// Commas inside quoted attribute values must not be treated as selector separators
|
||||
const el = document.querySelector('link[rel="preload"][as="image"][imagesrcset="url1.png 1x, url2.png 2x"]');
|
||||
testing.expectEqual('preload-link', el.id);
|
||||
|
||||
// Also test with single quotes inside selector
|
||||
const el2 = document.querySelector("link[imagesrcset='url1.png 1x, url2.png 2x']");
|
||||
testing.expectEqual('preload-link', el2.id);
|
||||
</script>
|
||||
|
||||
63
src/browser/tests/element/html/details.html
Normal file
63
src/browser/tests/element/html/details.html
Normal file
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<!-- Details elements -->
|
||||
<details id="details1">
|
||||
<summary>Summary</summary>
|
||||
Content
|
||||
</details>
|
||||
<details id="details2" open>
|
||||
<summary>Open Summary</summary>
|
||||
Content
|
||||
</details>
|
||||
|
||||
<script id="instanceof">
|
||||
{
|
||||
const details = document.createElement('details')
|
||||
testing.expectTrue(details instanceof HTMLDetailsElement)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="open_initial">
|
||||
testing.expectEqual(false, $('#details1').open)
|
||||
testing.expectEqual(true, $('#details2').open)
|
||||
</script>
|
||||
|
||||
<script id="open_set">
|
||||
{
|
||||
$('#details1').open = true
|
||||
testing.expectEqual(true, $('#details1').open)
|
||||
|
||||
$('#details2').open = false
|
||||
testing.expectEqual(false, $('#details2').open)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="open_reflects_attribute">
|
||||
{
|
||||
const details = document.createElement('details')
|
||||
testing.expectEqual(null, details.getAttribute('open'))
|
||||
|
||||
details.open = true
|
||||
testing.expectEqual('', details.getAttribute('open'))
|
||||
|
||||
details.open = false
|
||||
testing.expectEqual(null, details.getAttribute('open'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="name_initial">
|
||||
{
|
||||
const details = document.createElement('details')
|
||||
testing.expectEqual('', details.name)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="name_set">
|
||||
{
|
||||
const details = document.createElement('details')
|
||||
details.name = 'group1'
|
||||
testing.expectEqual('group1', details.name)
|
||||
testing.expectEqual('group1', details.getAttribute('name'))
|
||||
}
|
||||
</script>
|
||||
75
src/browser/tests/element/html/track.html
Normal file
75
src/browser/tests/element/html/track.html
Normal file
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<video id="video1">
|
||||
<track id="track1" kind="subtitles">
|
||||
<track id="track2" kind="captions">
|
||||
<track id="track3" kind="invalid-kind">
|
||||
</video>
|
||||
|
||||
<script id="instanceof">
|
||||
{
|
||||
const track = document.createElement("track");
|
||||
testing.expectEqual(true, track instanceof HTMLTrackElement);
|
||||
testing.expectEqual("[object HTMLTrackElement]", track.toString());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="kind_default">
|
||||
{
|
||||
const track = document.createElement("track");
|
||||
testing.expectEqual("subtitles", track.kind);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="kind_valid_values">
|
||||
{
|
||||
const track = document.createElement("track");
|
||||
|
||||
track.kind = "captions";
|
||||
testing.expectEqual("captions", track.kind);
|
||||
|
||||
track.kind = "descriptions";
|
||||
testing.expectEqual("descriptions", track.kind);
|
||||
|
||||
track.kind = "chapters";
|
||||
testing.expectEqual("chapters", track.kind);
|
||||
|
||||
track.kind = "metadata";
|
||||
testing.expectEqual("metadata", track.kind);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="kind_invalid">
|
||||
{
|
||||
const track = document.createElement("track");
|
||||
|
||||
track.kind = null;
|
||||
testing.expectEqual("metadata", track.kind);
|
||||
|
||||
track.kind = "Subtitles";
|
||||
testing.expectEqual("subtitles", track.kind);
|
||||
|
||||
track.kind = "";
|
||||
testing.expectEqual("metadata", track.kind);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="constants">
|
||||
{
|
||||
const track = document.createElement("track");
|
||||
testing.expectEqual(0, track.NONE);
|
||||
testing.expectEqual(1, track.LOADING);
|
||||
testing.expectEqual(2, track.LOADED);
|
||||
testing.expectEqual(3, track.ERROR);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="constants_static">
|
||||
{
|
||||
testing.expectEqual(0, HTMLTrackElement.NONE);
|
||||
testing.expectEqual(1, HTMLTrackElement.LOADING);
|
||||
testing.expectEqual(2, HTMLTrackElement.LOADED);
|
||||
testing.expectEqual(3, HTMLTrackElement.ERROR);
|
||||
}
|
||||
</script>
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<script>
|
||||
function frame1Onload() {
|
||||
window.f1_onload = true;
|
||||
window.f1_onload = 'f1_onload_loaded';
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
<iframe id=f2 src="support/sub2.html"></iframe>
|
||||
|
||||
<script id="basic">
|
||||
// reload it
|
||||
$('#f2').src = 'support/sub2.html';
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(undefined, window[10]);
|
||||
|
||||
@@ -47,8 +50,11 @@
|
||||
// child frame's top.parent is itself (root has no parent)
|
||||
testing.expectEqual(window, window[0].top.parent);
|
||||
|
||||
// Todo: Context security tokens
|
||||
// testing.expectEqual(true, window.sub1_loaded);
|
||||
// testing.expectEqual(true, window.sub2_loaded);
|
||||
// testing.expectEqual(1, window.sub1_count);
|
||||
// testing.expectEqual(2, window.sub2_count);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -63,14 +69,26 @@
|
||||
document.documentElement.appendChild(f3);
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(true, window.f1_onload);
|
||||
testing.expectEqual('f1_onload_loaded', window.f1_onload);
|
||||
testing.expectEqual(true, f3_load_event);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=onload>
|
||||
{
|
||||
let f4 = document.createElement('iframe');
|
||||
f4.src = "about:blank";
|
||||
document.documentElement.appendChild(f4);
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual("<html><head></head><body></body></html>", f4.contentDocument.documentElement.outerHTML);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=count>
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(3, window.length);
|
||||
testing.expectEqual(4, window.length);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
<script>
|
||||
// should not have access to the parent's JS context
|
||||
window.top.sub1_loaded = window.testing == undefined;
|
||||
window.top.sub1_count = (window.top.sub1_count || 0) + 1;
|
||||
</script>
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
<script>
|
||||
// should not have access to the parent's JS context
|
||||
window.top.sub2_loaded = window.testing == undefined;
|
||||
window.top.sub2_count = (window.top.sub2_count || 0) + 1;
|
||||
</script>
|
||||
|
||||
@@ -301,3 +301,74 @@
|
||||
testing.expectEqual(false, data3.done);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script id=enqueue_preserves_number>
|
||||
(async function() {
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(42);
|
||||
controller.enqueue(0);
|
||||
controller.enqueue(3.14);
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
|
||||
const reader = stream.getReader();
|
||||
|
||||
const r1 = await reader.read();
|
||||
testing.expectEqual(false, r1.done);
|
||||
testing.expectEqual('number', typeof r1.value);
|
||||
testing.expectEqual(42, r1.value);
|
||||
|
||||
const r2 = await reader.read();
|
||||
testing.expectEqual('number', typeof r2.value);
|
||||
testing.expectEqual(0, r2.value);
|
||||
|
||||
const r3 = await reader.read();
|
||||
testing.expectEqual('number', typeof r3.value);
|
||||
testing.expectEqual(3.14, r3.value);
|
||||
|
||||
const r4 = await reader.read();
|
||||
testing.expectEqual(true, r4.done);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script id=enqueue_preserves_bool>
|
||||
(async function() {
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(true);
|
||||
controller.enqueue(false);
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
|
||||
const reader = stream.getReader();
|
||||
|
||||
const r1 = await reader.read();
|
||||
testing.expectEqual('boolean', typeof r1.value);
|
||||
testing.expectEqual(true, r1.value);
|
||||
|
||||
const r2 = await reader.read();
|
||||
testing.expectEqual('boolean', typeof r2.value);
|
||||
testing.expectEqual(false, r2.value);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script id=enqueue_preserves_object>
|
||||
(async function() {
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue({ key: 'value', num: 7 });
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
|
||||
const reader = stream.getReader();
|
||||
|
||||
const r1 = await reader.read();
|
||||
testing.expectEqual('object', typeof r1.value);
|
||||
testing.expectEqual('value', r1.value.key);
|
||||
testing.expectEqual(7, r1.value.num);
|
||||
})();
|
||||
</script>
|
||||
|
||||
82
src/browser/tests/streams/text_decoder_stream.html
Normal file
82
src/browser/tests/streams/text_decoder_stream.html
Normal file
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id=text_decoder_stream_encoding>
|
||||
{
|
||||
const tds = new TextDecoderStream();
|
||||
testing.expectEqual('utf-8', tds.encoding);
|
||||
testing.expectEqual('object', typeof tds.readable);
|
||||
testing.expectEqual('object', typeof tds.writable);
|
||||
testing.expectEqual(false, tds.fatal);
|
||||
testing.expectEqual(false, tds.ignoreBOM);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=text_decoder_stream_with_label>
|
||||
{
|
||||
const tds = new TextDecoderStream('utf-8');
|
||||
testing.expectEqual('utf-8', tds.encoding);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=text_decoder_stream_with_opts>
|
||||
{
|
||||
const tds = new TextDecoderStream('utf-8', { fatal: true, ignoreBOM: true });
|
||||
testing.expectEqual(true, tds.fatal);
|
||||
testing.expectEqual(true, tds.ignoreBOM);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=text_decoder_stream_invalid_label>
|
||||
{
|
||||
let errorThrown = false;
|
||||
try {
|
||||
new TextDecoderStream('windows-1252');
|
||||
} catch (e) {
|
||||
errorThrown = true;
|
||||
}
|
||||
testing.expectEqual(true, errorThrown);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=text_decoder_stream_decode>
|
||||
(async function() {
|
||||
const tds = new TextDecoderStream();
|
||||
|
||||
const writer = tds.writable.getWriter();
|
||||
const reader = tds.readable.getReader();
|
||||
|
||||
// 'hello' in UTF-8 bytes
|
||||
const bytes = new Uint8Array([104, 101, 108, 108, 111]);
|
||||
await writer.write(bytes);
|
||||
await writer.close();
|
||||
|
||||
const result = await reader.read();
|
||||
testing.expectEqual(false, result.done);
|
||||
testing.expectEqual('hello', result.value);
|
||||
|
||||
const result2 = await reader.read();
|
||||
testing.expectEqual(true, result2.done);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script id=text_decoder_stream_empty_chunk>
|
||||
(async function() {
|
||||
const tds = new TextDecoderStream();
|
||||
const writer = tds.writable.getWriter();
|
||||
const reader = tds.readable.getReader();
|
||||
|
||||
// Write an empty chunk followed by real data
|
||||
await writer.write(new Uint8Array([]));
|
||||
await writer.write(new Uint8Array([104, 105]));
|
||||
await writer.close();
|
||||
|
||||
// Empty chunk should be filtered out; first read gets "hi"
|
||||
const result = await reader.read();
|
||||
testing.expectEqual(false, result.done);
|
||||
testing.expectEqual('hi', result.value);
|
||||
|
||||
const result2 = await reader.read();
|
||||
testing.expectEqual(true, result2.done);
|
||||
})();
|
||||
</script>
|
||||
164
src/browser/tests/streams/transform_stream.html
Normal file
164
src/browser/tests/streams/transform_stream.html
Normal file
@@ -0,0 +1,164 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id=transform_stream_basic>
|
||||
{
|
||||
const ts = new TransformStream();
|
||||
testing.expectEqual('object', typeof ts);
|
||||
testing.expectEqual('object', typeof ts.readable);
|
||||
testing.expectEqual('object', typeof ts.writable);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=transform_stream_with_transformer>
|
||||
(async function() {
|
||||
const ts = new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
controller.enqueue(chunk.toUpperCase());
|
||||
}
|
||||
});
|
||||
|
||||
const writer = ts.writable.getWriter();
|
||||
const reader = ts.readable.getReader();
|
||||
|
||||
await writer.write('hello');
|
||||
await writer.close();
|
||||
|
||||
const result = await reader.read();
|
||||
testing.expectEqual(false, result.done);
|
||||
testing.expectEqual('HELLO', result.value);
|
||||
|
||||
const result2 = await reader.read();
|
||||
testing.expectEqual(true, result2.done);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script id=writable_stream_basic>
|
||||
{
|
||||
const ws = new WritableStream();
|
||||
testing.expectEqual('object', typeof ws);
|
||||
testing.expectEqual(false, ws.locked);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=writable_stream_writer>
|
||||
{
|
||||
const ws = new WritableStream();
|
||||
const writer = ws.getWriter();
|
||||
testing.expectEqual('object', typeof writer);
|
||||
testing.expectEqual(true, ws.locked);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=writable_stream_writer_desired_size>
|
||||
{
|
||||
const ws = new WritableStream();
|
||||
const writer = ws.getWriter();
|
||||
testing.expectEqual(1, writer.desiredSize);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=text_encoder_stream_encoding>
|
||||
{
|
||||
const tes = new TextEncoderStream();
|
||||
testing.expectEqual('utf-8', tes.encoding);
|
||||
testing.expectEqual('object', typeof tes.readable);
|
||||
testing.expectEqual('object', typeof tes.writable);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=text_encoder_stream_encode>
|
||||
(async function() {
|
||||
const tes = new TextEncoderStream();
|
||||
|
||||
const writer = tes.writable.getWriter();
|
||||
const reader = tes.readable.getReader();
|
||||
|
||||
await writer.write('hi');
|
||||
await writer.close();
|
||||
|
||||
const result = await reader.read();
|
||||
testing.expectEqual(false, result.done);
|
||||
testing.expectEqual(true, result.value instanceof Uint8Array);
|
||||
// 'hi' in UTF-8 is [104, 105]
|
||||
testing.expectEqual(104, result.value[0]);
|
||||
testing.expectEqual(105, result.value[1]);
|
||||
testing.expectEqual(2, result.value.length);
|
||||
|
||||
const result2 = await reader.read();
|
||||
testing.expectEqual(true, result2.done);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script id=pipe_through_basic>
|
||||
(async function() {
|
||||
const input = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue('hello');
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
|
||||
const ts = new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
controller.enqueue(chunk.toUpperCase());
|
||||
}
|
||||
});
|
||||
|
||||
const output = input.pipeThrough(ts);
|
||||
const reader = output.getReader();
|
||||
|
||||
const result = await reader.read();
|
||||
testing.expectEqual(false, result.done);
|
||||
testing.expectEqual('HELLO', result.value);
|
||||
|
||||
const result2 = await reader.read();
|
||||
testing.expectEqual(true, result2.done);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script id=pipe_to_basic>
|
||||
(async function() {
|
||||
const chunks = [];
|
||||
const input = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue('a');
|
||||
controller.enqueue('b');
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
|
||||
const ws = new WritableStream({
|
||||
write(chunk) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
});
|
||||
|
||||
await input.pipeTo(ws);
|
||||
testing.expectEqual(2, chunks.length);
|
||||
testing.expectEqual('a', chunks[0]);
|
||||
testing.expectEqual('b', chunks[1]);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script id=pipe_through_text_decoder>
|
||||
(async function() {
|
||||
const bytes = new Uint8Array([104, 101, 108, 108, 111]);
|
||||
const input = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(bytes);
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
|
||||
const output = input.pipeThrough(new TextDecoderStream());
|
||||
const reader = output.getReader();
|
||||
|
||||
const result = await reader.read();
|
||||
testing.expectEqual(false, result.done);
|
||||
testing.expectEqual('hello', result.value);
|
||||
|
||||
const result2 = await reader.read();
|
||||
testing.expectEqual(true, result2.done);
|
||||
})();
|
||||
</script>
|
||||
@@ -209,6 +209,7 @@ pub fn getTagNameLower(self: *const Element) []const u8 {
|
||||
.custom => |e| e._tag_name.str(),
|
||||
.data => "data",
|
||||
.datalist => "datalist",
|
||||
.details => "details",
|
||||
.dialog => "dialog",
|
||||
.directory => "dir",
|
||||
.div => "div",
|
||||
@@ -287,6 +288,7 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {
|
||||
.custom => |e| upperTagName(&e._tag_name, buf),
|
||||
.data => "DATA",
|
||||
.datalist => "DATALIST",
|
||||
.details => "DETAILS",
|
||||
.dialog => "DIALOG",
|
||||
.directory => "DIR",
|
||||
.div => "DIV",
|
||||
@@ -1327,9 +1329,18 @@ pub fn clone(self: *Element, deep: bool, page: *Page) !*Node {
|
||||
var child_it = self.asNode().childrenIterator();
|
||||
while (child_it.next()) |child| {
|
||||
const cloned_child = try child.cloneNode(true, page);
|
||||
if (cloned_child._parent != null) {
|
||||
// This is almost always false, the only case where a cloned
|
||||
// node would already have a parent is with a custom element
|
||||
// that has a constructor (which is called during cloning) which
|
||||
// inserts it somewhere. In that case, whatever parent was set
|
||||
// in the constructor should not be changed.
|
||||
continue;
|
||||
}
|
||||
|
||||
// We pass `true` to `child_already_connected` as a hacky optimization
|
||||
// We _know_ this child isn't connected (Becasue the parent isn't connected)
|
||||
// setting this to `true` skips all connection checks and just assumes t
|
||||
// We _know_ this child isn't connected (Because the parent isn't connected)
|
||||
// setting this to `true` skips all connection checks.
|
||||
try page.appendNode(node, cloned_child, .{ .child_already_connected = true });
|
||||
}
|
||||
}
|
||||
@@ -1385,6 +1396,7 @@ pub fn getTag(self: *const Element) Tag {
|
||||
.custom => .custom,
|
||||
.data => .data,
|
||||
.datalist => .datalist,
|
||||
.details => .details,
|
||||
.dialog => .dialog,
|
||||
.directory => .directory,
|
||||
.iframe => .iframe,
|
||||
|
||||
@@ -255,7 +255,7 @@ fn getDefaultDisplay(element: *const Element) []const u8 {
|
||||
.html => |html| {
|
||||
return switch (html._type) {
|
||||
.anchor, .br, .span, .label, .time, .font, .mod, .quote => "inline",
|
||||
.body, .div, .dl, .p, .heading, .form, .button, .canvas, .dialog, .embed, .head, .html, .hr, .iframe, .img, .input, .li, .link, .meta, .ol, .option, .script, .select, .slot, .style, .template, .textarea, .title, .ul, .media, .area, .base, .datalist, .directory, .fieldset, .legend, .map, .meter, .object, .optgroup, .output, .param, .picture, .pre, .progress, .source, .table, .table_caption, .table_cell, .table_col, .table_row, .table_section, .track => "block",
|
||||
.body, .div, .dl, .p, .heading, .form, .button, .canvas, .details, .dialog, .embed, .head, .html, .hr, .iframe, .img, .input, .li, .link, .meta, .ol, .option, .script, .select, .slot, .style, .template, .textarea, .title, .ul, .media, .area, .base, .datalist, .directory, .fieldset, .legend, .map, .meter, .object, .optgroup, .output, .param, .picture, .pre, .progress, .source, .table, .table_caption, .table_cell, .table_col, .table_row, .table_section, .track => "block",
|
||||
.generic, .custom, .unknown, .data => blk: {
|
||||
const tag = element.getTagNameLower();
|
||||
if (isInlineTag(tag)) break :blk "inline";
|
||||
|
||||
@@ -39,6 +39,7 @@ pub const Canvas = @import("html/Canvas.zig");
|
||||
pub const Custom = @import("html/Custom.zig");
|
||||
pub const Data = @import("html/Data.zig");
|
||||
pub const DataList = @import("html/DataList.zig");
|
||||
pub const Details = @import("html/Details.zig");
|
||||
pub const Dialog = @import("html/Dialog.zig");
|
||||
pub const Directory = @import("html/Directory.zig");
|
||||
pub const Div = @import("html/Div.zig");
|
||||
@@ -119,6 +120,7 @@ pub const Type = union(enum) {
|
||||
custom: *Custom,
|
||||
data: *Data,
|
||||
datalist: *DataList,
|
||||
details: *Details,
|
||||
dialog: *Dialog,
|
||||
directory: *Directory,
|
||||
div: *Div,
|
||||
|
||||
58
src/browser/webapi/element/html/Details.zig
Normal file
58
src/browser/webapi/element/html/Details.zig
Normal file
@@ -0,0 +1,58 @@
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Page = @import("../../../Page.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
|
||||
const Details = @This();
|
||||
|
||||
_proto: *HtmlElement,
|
||||
|
||||
pub fn asElement(self: *Details) *Element {
|
||||
return self._proto._proto;
|
||||
}
|
||||
pub fn asConstElement(self: *const Details) *const Element {
|
||||
return self._proto._proto;
|
||||
}
|
||||
pub fn asNode(self: *Details) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub fn getOpen(self: *const Details) bool {
|
||||
return self.asConstElement().getAttributeSafe(comptime .wrap("open")) != null;
|
||||
}
|
||||
|
||||
pub fn setOpen(self: *Details, open: bool, page: *Page) !void {
|
||||
if (open) {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("open"), .wrap(""), page);
|
||||
} else {
|
||||
try self.asElement().removeAttribute(comptime .wrap("open"), page);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getName(self: *const Details) []const u8 {
|
||||
return self.asConstElement().getAttributeSafe(comptime .wrap("name")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setName(self: *Details, value: []const u8, page: *Page) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("name"), .wrap(value), page);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Details);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "HTMLDetailsElement";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const open = bridge.accessor(Details.getOpen, Details.setOpen, .{});
|
||||
pub const name = bridge.accessor(Details.getName, Details.setName, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../../../testing.zig");
|
||||
test "WebApi: HTML.Details" {
|
||||
try testing.htmlRunner("element/html/details.html", .{});
|
||||
}
|
||||
@@ -58,6 +58,9 @@ pub fn setSrc(self: *IFrame, src: []const u8, page: *Page) !void {
|
||||
try element.setAttributeSafe(comptime .wrap("src"), .wrap(src), page);
|
||||
self._src = element.getAttributeSafe(comptime .wrap("src")) orelse unreachable;
|
||||
if (element.asNode().isConnected()) {
|
||||
// unlike script, an iframe is reloaded every time the src is set
|
||||
// even if it's set to the same URL.
|
||||
self._executed = false;
|
||||
try page.iframeAddedCallback(self);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
// Copyright (C) 2023-2026 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 js = @import("../../../js/js.zig");
|
||||
const String = @import("../../../../string.zig").String;
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
@@ -6,6 +28,10 @@ const HtmlElement = @import("../Html.zig");
|
||||
const Track = @This();
|
||||
|
||||
_proto: *HtmlElement,
|
||||
_kind: String,
|
||||
_ready_state: ReadyState,
|
||||
|
||||
const ReadyState = enum(u8) { none, loading, loaded, @"error" };
|
||||
|
||||
pub fn asElement(self: *Track) *Element {
|
||||
return self._proto._proto;
|
||||
@@ -14,6 +40,38 @@ pub fn asNode(self: *Track) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub fn setKind(self: *Track, maybe_kind: ?String) void {
|
||||
const kind = maybe_kind orelse {
|
||||
self._kind = comptime .wrap("metadata");
|
||||
return;
|
||||
};
|
||||
|
||||
// Special case, for some reason, FF does this case-insensitive.
|
||||
if (std.ascii.eqlIgnoreCase(kind.str(), "subtitles")) {
|
||||
self._kind = comptime .wrap("subtitles");
|
||||
return;
|
||||
}
|
||||
if (kind.eql(comptime .wrap("captions"))) {
|
||||
self._kind = comptime .wrap("captions");
|
||||
return;
|
||||
}
|
||||
if (kind.eql(comptime .wrap("descriptions"))) {
|
||||
self._kind = comptime .wrap("descriptions");
|
||||
return;
|
||||
}
|
||||
if (kind.eql(comptime .wrap("chapters"))) {
|
||||
self._kind = comptime .wrap("chapters");
|
||||
return;
|
||||
}
|
||||
|
||||
// Anything else must be considered as `metadata`.
|
||||
self._kind = comptime .wrap("metadata");
|
||||
}
|
||||
|
||||
pub fn getKind(self: *const Track) String {
|
||||
return self._kind;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Track);
|
||||
|
||||
@@ -22,4 +80,16 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const kind = bridge.accessor(Track.getKind, Track.setKind, .{});
|
||||
|
||||
pub const NONE = bridge.property(@as(u16, @intFromEnum(ReadyState.none)), .{ .template = true });
|
||||
pub const LOADING = bridge.property(@as(u16, @intFromEnum(ReadyState.loading)), .{ .template = true });
|
||||
pub const LOADED = bridge.property(@as(u16, @intFromEnum(ReadyState.loaded)), .{ .template = true });
|
||||
pub const ERROR = bridge.property(@as(u16, @intFromEnum(ReadyState.@"error")), .{ .template = true });
|
||||
};
|
||||
|
||||
const testing = @import("../../../../testing.zig");
|
||||
test "WebApi: HTML.Track" {
|
||||
try testing.htmlRunner("element/html/track.html", .{});
|
||||
}
|
||||
|
||||
127
src/browser/webapi/encoding/TextDecoderStream.zig
Normal file
127
src/browser/webapi/encoding/TextDecoderStream.zig
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright (C) 2023-2026 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 js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
const ReadableStream = @import("../streams/ReadableStream.zig");
|
||||
const WritableStream = @import("../streams/WritableStream.zig");
|
||||
const TransformStream = @import("../streams/TransformStream.zig");
|
||||
|
||||
const TextDecoderStream = @This();
|
||||
|
||||
_transform: *TransformStream,
|
||||
_fatal: bool,
|
||||
_ignore_bom: bool,
|
||||
|
||||
const Label = enum {
|
||||
utf8,
|
||||
@"utf-8",
|
||||
@"unicode-1-1-utf-8",
|
||||
};
|
||||
|
||||
const InitOpts = struct {
|
||||
fatal: bool = false,
|
||||
ignoreBOM: bool = false,
|
||||
};
|
||||
|
||||
pub fn init(label_: ?[]const u8, opts_: ?InitOpts, page: *Page) !TextDecoderStream {
|
||||
if (label_) |label| {
|
||||
_ = std.meta.stringToEnum(Label, label) orelse return error.RangeError;
|
||||
}
|
||||
|
||||
const opts = opts_ orelse InitOpts{};
|
||||
const decodeFn: TransformStream.ZigTransformFn = blk: {
|
||||
if (opts.ignoreBOM) {
|
||||
break :blk struct {
|
||||
fn decode(controller: *TransformStream.DefaultController, chunk: js.Value) !void {
|
||||
return decodeTransform(controller, chunk, true);
|
||||
}
|
||||
}.decode;
|
||||
} else {
|
||||
break :blk struct {
|
||||
fn decode(controller: *TransformStream.DefaultController, chunk: js.Value) !void {
|
||||
return decodeTransform(controller, chunk, false);
|
||||
}
|
||||
}.decode;
|
||||
}
|
||||
};
|
||||
|
||||
const transform = try TransformStream.initWithZigTransform(decodeFn, page);
|
||||
|
||||
return .{
|
||||
._transform = transform,
|
||||
._fatal = opts.fatal,
|
||||
._ignore_bom = opts.ignoreBOM,
|
||||
};
|
||||
}
|
||||
|
||||
fn decodeTransform(controller: *TransformStream.DefaultController, chunk: js.Value, ignoreBOM: bool) !void {
|
||||
// chunk should be a Uint8Array; decode it as UTF-8 string
|
||||
const typed_array = try chunk.toZig(js.TypedArray(u8));
|
||||
var input = typed_array.values;
|
||||
|
||||
// Strip UTF-8 BOM if present
|
||||
if (ignoreBOM == false and std.mem.startsWith(u8, input, &.{ 0xEF, 0xBB, 0xBF })) {
|
||||
input = input[3..];
|
||||
}
|
||||
|
||||
// Per spec, empty chunks produce no output
|
||||
if (input.len == 0) return;
|
||||
|
||||
try controller.enqueue(.{ .string = input });
|
||||
}
|
||||
|
||||
pub fn getReadable(self: *const TextDecoderStream) *ReadableStream {
|
||||
return self._transform.getReadable();
|
||||
}
|
||||
|
||||
pub fn getWritable(self: *const TextDecoderStream) *WritableStream {
|
||||
return self._transform.getWritable();
|
||||
}
|
||||
|
||||
pub fn getFatal(self: *const TextDecoderStream) bool {
|
||||
return self._fatal;
|
||||
}
|
||||
|
||||
pub fn getIgnoreBOM(self: *const TextDecoderStream) bool {
|
||||
return self._ignore_bom;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(TextDecoderStream);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "TextDecoderStream";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(TextDecoderStream.init, .{});
|
||||
pub const encoding = bridge.property("utf-8", .{ .template = false });
|
||||
pub const readable = bridge.accessor(TextDecoderStream.getReadable, null, .{});
|
||||
pub const writable = bridge.accessor(TextDecoderStream.getWritable, null, .{});
|
||||
pub const fatal = bridge.accessor(TextDecoderStream.getFatal, null, .{});
|
||||
pub const ignoreBOM = bridge.accessor(TextDecoderStream.getIgnoreBOM, null, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../../testing.zig");
|
||||
test "WebApi: TextDecoderStream" {
|
||||
try testing.htmlRunner("streams/text_decoder_stream.html", .{});
|
||||
}
|
||||
70
src/browser/webapi/encoding/TextEncoderStream.zig
Normal file
70
src/browser/webapi/encoding/TextEncoderStream.zig
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright (C) 2023-2026 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 js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
const ReadableStream = @import("../streams/ReadableStream.zig");
|
||||
const WritableStream = @import("../streams/WritableStream.zig");
|
||||
const TransformStream = @import("../streams/TransformStream.zig");
|
||||
|
||||
const TextEncoderStream = @This();
|
||||
|
||||
_transform: *TransformStream,
|
||||
|
||||
pub fn init(page: *Page) !TextEncoderStream {
|
||||
const transform = try TransformStream.initWithZigTransform(&encodeTransform, page);
|
||||
return .{
|
||||
._transform = transform,
|
||||
};
|
||||
}
|
||||
|
||||
fn encodeTransform(controller: *TransformStream.DefaultController, chunk: js.Value) !void {
|
||||
// chunk should be a JS string; encode it as UTF-8 bytes (Uint8Array)
|
||||
const str = chunk.isString() orelse return error.InvalidChunk;
|
||||
const slice = try str.toSlice();
|
||||
try controller.enqueue(.{ .uint8array = .{ .values = slice } });
|
||||
}
|
||||
|
||||
pub fn getReadable(self: *const TextEncoderStream) *ReadableStream {
|
||||
return self._transform.getReadable();
|
||||
}
|
||||
|
||||
pub fn getWritable(self: *const TextEncoderStream) *WritableStream {
|
||||
return self._transform.getWritable();
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(TextEncoderStream);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "TextEncoderStream";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(TextEncoderStream.init, .{});
|
||||
pub const encoding = bridge.property("utf-8", .{ .template = false });
|
||||
pub const readable = bridge.accessor(TextEncoderStream.getReadable, null, .{});
|
||||
pub const writable = bridge.accessor(TextEncoderStream.getWritable, null, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../../testing.zig");
|
||||
test "WebApi: TextEncoderStream" {
|
||||
try testing.htmlRunner("streams/transform_stream.html", .{});
|
||||
}
|
||||
@@ -87,15 +87,35 @@ pub fn parseList(arena: Allocator, input: []const u8, page: *Page) ParseError![]
|
||||
|
||||
var comma_pos: usize = trimmed.len;
|
||||
var depth: usize = 0;
|
||||
var in_quote: u8 = 0; // 0 = not in quotes, '"' or '\'' = in that quote type
|
||||
var i: usize = 0;
|
||||
while (i < trimmed.len) {
|
||||
const c = trimmed[i];
|
||||
if (in_quote != 0) {
|
||||
// Inside a quoted string
|
||||
if (c == '\\') {
|
||||
// Skip escape sequence inside quotes
|
||||
i += 1;
|
||||
if (i < trimmed.len) i += 1;
|
||||
} else if (c == in_quote) {
|
||||
// Closing quote
|
||||
in_quote = 0;
|
||||
i += 1;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
switch (c) {
|
||||
'\\' => {
|
||||
// Skip escape sequence (backslash + next character)
|
||||
i += 1;
|
||||
if (i < trimmed.len) i += 1;
|
||||
},
|
||||
'"', '\'' => {
|
||||
in_quote = c;
|
||||
i += 1;
|
||||
},
|
||||
'(' => {
|
||||
depth += 1;
|
||||
i += 1;
|
||||
|
||||
@@ -24,6 +24,7 @@ const Page = @import("../../Page.zig");
|
||||
|
||||
const ReadableStreamDefaultReader = @import("ReadableStreamDefaultReader.zig");
|
||||
const ReadableStreamDefaultController = @import("ReadableStreamDefaultController.zig");
|
||||
const WritableStream = @import("WritableStream.zig");
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
@@ -233,6 +234,126 @@ pub fn cancel(self: *ReadableStream, reason: ?[]const u8, page: *Page) !js.Promi
|
||||
return resolver.promise();
|
||||
}
|
||||
|
||||
/// pipeThrough(transform) — pipes this readable stream through a transform stream,
|
||||
/// returning the readable side. `transform` is a JS object with `readable` and `writable` properties.
|
||||
const PipeTransform = struct {
|
||||
writable: *WritableStream,
|
||||
readable: *ReadableStream,
|
||||
};
|
||||
pub fn pipeThrough(self: *ReadableStream, transform: PipeTransform, page: *Page) !*ReadableStream {
|
||||
if (self.getLocked()) {
|
||||
return error.ReaderLocked;
|
||||
}
|
||||
|
||||
// Start async piping from this stream to the writable side
|
||||
try PipeState.startPipe(self, transform.writable, null, page);
|
||||
|
||||
return transform.readable;
|
||||
}
|
||||
|
||||
/// pipeTo(writable) — pipes this readable stream to a writable stream.
|
||||
/// Returns a promise that resolves when piping is complete.
|
||||
pub fn pipeTo(self: *ReadableStream, destination: *WritableStream, page: *Page) !js.Promise {
|
||||
if (self.getLocked()) {
|
||||
return page.js.local.?.rejectPromise("ReadableStream is locked");
|
||||
}
|
||||
|
||||
const local = page.js.local.?;
|
||||
var pipe_resolver = local.createPromiseResolver();
|
||||
const promise = pipe_resolver.promise();
|
||||
const persisted_resolver = try pipe_resolver.persist();
|
||||
|
||||
try PipeState.startPipe(self, destination, persisted_resolver, page);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/// State for an async pipe operation.
|
||||
const PipeState = struct {
|
||||
reader: *ReadableStreamDefaultReader,
|
||||
writable: *WritableStream,
|
||||
context_id: usize,
|
||||
resolver: ?js.PromiseResolver.Global,
|
||||
|
||||
fn startPipe(
|
||||
stream: *ReadableStream,
|
||||
writable: *WritableStream,
|
||||
resolver: ?js.PromiseResolver.Global,
|
||||
page: *Page,
|
||||
) !void {
|
||||
const reader = try stream.getReader(page);
|
||||
const state = try page.arena.create(PipeState);
|
||||
state.* = .{
|
||||
.reader = reader,
|
||||
.writable = writable,
|
||||
.context_id = page.js.id,
|
||||
.resolver = resolver,
|
||||
};
|
||||
try state.pumpRead(page);
|
||||
}
|
||||
|
||||
fn pumpRead(state: *PipeState, page: *Page) !void {
|
||||
const local = page.js.local.?;
|
||||
|
||||
// Call reader.read() which returns a Promise
|
||||
const read_promise = try state.reader.read(page);
|
||||
|
||||
// Create JS callback functions for .then() and .catch()
|
||||
const then_fn = local.newCallback(onReadFulfilled, state);
|
||||
const catch_fn = local.newCallback(onReadRejected, state);
|
||||
|
||||
_ = read_promise.thenAndCatch(then_fn, catch_fn) catch {
|
||||
state.finish(local);
|
||||
};
|
||||
}
|
||||
|
||||
const ReadData = struct {
|
||||
done: bool,
|
||||
value: js.Value,
|
||||
};
|
||||
fn onReadFulfilled(self: *PipeState, data_: ?ReadData, page: *Page) void {
|
||||
const local = page.js.local.?;
|
||||
const data = data_ orelse {
|
||||
return self.finish(local);
|
||||
};
|
||||
|
||||
if (data.done) {
|
||||
// Stream is finished, close the writable side
|
||||
self.writable.closeStream(page) catch {};
|
||||
self.reader.releaseLock();
|
||||
if (self.resolver) |r| {
|
||||
local.toLocal(r).resolve("pipeTo complete", {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const value = data.value;
|
||||
if (value.isUndefined()) {
|
||||
return self.finish(local);
|
||||
}
|
||||
|
||||
self.writable.writeChunk(value, page) catch {
|
||||
return self.finish(local);
|
||||
};
|
||||
|
||||
// Continue reading the next chunk
|
||||
self.pumpRead(page) catch {
|
||||
self.finish(local);
|
||||
};
|
||||
}
|
||||
|
||||
fn onReadRejected(self: *PipeState, page: *Page) void {
|
||||
self.finish(page.js.local.?);
|
||||
}
|
||||
|
||||
fn finish(self: *PipeState, local: *const js.Local) void {
|
||||
self.reader.releaseLock();
|
||||
if (self.resolver) |r| {
|
||||
local.toLocal(r).resolve("pipe finished", {});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Cancel = struct {
|
||||
callback: ?js.Function.Global = null,
|
||||
reason: ?[]const u8 = null,
|
||||
@@ -251,6 +372,8 @@ pub const JsApi = struct {
|
||||
pub const constructor = bridge.constructor(ReadableStream.init, .{});
|
||||
pub const cancel = bridge.function(ReadableStream.cancel, .{});
|
||||
pub const getReader = bridge.function(ReadableStream.getReader, .{});
|
||||
pub const pipeThrough = bridge.function(ReadableStream.pipeThrough, .{});
|
||||
pub const pipeTo = bridge.function(ReadableStream.pipeTo, .{});
|
||||
pub const locked = bridge.accessor(ReadableStream.getLocked, null, .{});
|
||||
pub const symbol_async_iterator = bridge.iterator(ReadableStream.getAsyncIterator, .{ .async = true });
|
||||
};
|
||||
|
||||
@@ -33,11 +33,13 @@ pub const Chunk = union(enum) {
|
||||
// the order matters, sorry.
|
||||
uint8array: js.TypedArray(u8),
|
||||
string: []const u8,
|
||||
js_value: js.Value.Global,
|
||||
|
||||
pub fn dupe(self: Chunk, allocator: std.mem.Allocator) !Chunk {
|
||||
return switch (self) {
|
||||
.string => |str| .{ .string = try allocator.dupe(u8, str) },
|
||||
.uint8array => |arr| .{ .uint8array = try arr.dupe(allocator) },
|
||||
.js_value => |val| .{ .js_value = val },
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -98,6 +100,40 @@ pub fn enqueue(self: *ReadableStreamDefaultController, chunk: Chunk) !void {
|
||||
ls.toLocal(resolver).resolve("stream enqueue", result);
|
||||
}
|
||||
|
||||
/// Enqueue a raw JS value, preserving its type (number, bool, object, etc.).
|
||||
/// Used by the JS-facing API; internal Zig callers should use enqueue(Chunk).
|
||||
pub fn enqueueValue(self: *ReadableStreamDefaultController, value: js.Value) !void {
|
||||
if (self._stream._state != .readable) {
|
||||
return error.StreamNotReadable;
|
||||
}
|
||||
|
||||
if (self._pending_reads.items.len == 0) {
|
||||
const persisted = try value.persist();
|
||||
try self._queue.append(self._arena, .{ .js_value = persisted });
|
||||
return;
|
||||
}
|
||||
|
||||
const resolver = self._pending_reads.orderedRemove(0);
|
||||
const persisted = try value.persist();
|
||||
const result = ReadableStreamDefaultReader.ReadResult{
|
||||
.done = false,
|
||||
.value = .{ .js_value = persisted },
|
||||
};
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
if (self._page.js.local == null) {
|
||||
log.fatal(.bug, "null context scope", .{ .src = "ReadableStreamDefaultController.enqueueValue", .url = self._page.url });
|
||||
std.debug.assert(self._page.js.local != null);
|
||||
}
|
||||
}
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
self._page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
ls.toLocal(resolver).resolve("stream enqueue value", result);
|
||||
}
|
||||
|
||||
pub fn close(self: *ReadableStreamDefaultController) !void {
|
||||
if (self._stream._state != .readable) {
|
||||
return error.StreamNotReadable;
|
||||
@@ -176,7 +212,7 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const enqueue = bridge.function(ReadableStreamDefaultController.enqueue, .{});
|
||||
pub const enqueue = bridge.function(ReadableStreamDefaultController.enqueueValue, .{});
|
||||
pub const close = bridge.function(ReadableStreamDefaultController.close, .{});
|
||||
pub const @"error" = bridge.function(ReadableStreamDefaultController.doError, .{});
|
||||
pub const desiredSize = bridge.accessor(ReadableStreamDefaultController.getDesiredSize, null, .{});
|
||||
|
||||
@@ -44,11 +44,13 @@ pub const ReadResult = struct {
|
||||
empty,
|
||||
string: []const u8,
|
||||
uint8array: js.TypedArray(u8),
|
||||
js_value: js.Value.Global,
|
||||
|
||||
pub fn fromChunk(chunk: ReadableStreamDefaultController.Chunk) Chunk {
|
||||
return switch (chunk) {
|
||||
.string => |s| .{ .string = s },
|
||||
.uint8array => |arr| .{ .uint8array = arr },
|
||||
.js_value => |val| .{ .js_value = val },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
198
src/browser/webapi/streams/TransformStream.zig
Normal file
198
src/browser/webapi/streams/TransformStream.zig
Normal file
@@ -0,0 +1,198 @@
|
||||
// Copyright (C) 2023-2026 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 js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
const ReadableStream = @import("ReadableStream.zig");
|
||||
const ReadableStreamDefaultController = @import("ReadableStreamDefaultController.zig");
|
||||
const WritableStream = @import("WritableStream.zig");
|
||||
|
||||
const TransformStream = @This();
|
||||
|
||||
pub const DefaultController = TransformStreamDefaultController;
|
||||
|
||||
pub const ZigTransformFn = *const fn (*TransformStreamDefaultController, js.Value) anyerror!void;
|
||||
|
||||
_readable: *ReadableStream,
|
||||
_writable: *WritableStream,
|
||||
_controller: *TransformStreamDefaultController,
|
||||
|
||||
const Transformer = struct {
|
||||
start: ?js.Function = null,
|
||||
transform: ?js.Function.Global = null,
|
||||
flush: ?js.Function.Global = null,
|
||||
};
|
||||
|
||||
pub fn init(transformer_: ?Transformer, page: *Page) !*TransformStream {
|
||||
const readable = try ReadableStream.init(null, null, page);
|
||||
|
||||
const self = try page._factory.create(TransformStream{
|
||||
._readable = readable,
|
||||
._writable = undefined,
|
||||
._controller = undefined,
|
||||
});
|
||||
|
||||
const transform_controller = try TransformStreamDefaultController.init(
|
||||
self,
|
||||
if (transformer_) |t| t.transform else null,
|
||||
if (transformer_) |t| t.flush else null,
|
||||
null,
|
||||
page,
|
||||
);
|
||||
self._controller = transform_controller;
|
||||
|
||||
self._writable = try WritableStream.initForTransform(self, page);
|
||||
|
||||
if (transformer_) |transformer| {
|
||||
if (transformer.start) |start| {
|
||||
try start.call(void, .{transform_controller});
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn initWithZigTransform(zig_transform: ZigTransformFn, page: *Page) !*TransformStream {
|
||||
const readable = try ReadableStream.init(null, null, page);
|
||||
|
||||
const self = try page._factory.create(TransformStream{
|
||||
._readable = readable,
|
||||
._writable = undefined,
|
||||
._controller = undefined,
|
||||
});
|
||||
|
||||
const transform_controller = try TransformStreamDefaultController.init(self, null, null, zig_transform, page);
|
||||
self._controller = transform_controller;
|
||||
|
||||
self._writable = try WritableStream.initForTransform(self, page);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn transformWrite(self: *TransformStream, chunk: js.Value, page: *Page) !void {
|
||||
if (self._controller._zig_transform_fn) |zig_fn| {
|
||||
// Zig-level transform (used by TextEncoderStream etc.)
|
||||
try zig_fn(self._controller, chunk);
|
||||
return;
|
||||
}
|
||||
|
||||
if (self._controller._transform_fn) |transform_fn| {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
try ls.toLocal(transform_fn).call(void, .{ chunk, self._controller });
|
||||
} else {
|
||||
try self._readable._controller.enqueue(.{ .string = try chunk.toStringSlice() });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transformClose(self: *TransformStream, page: *Page) !void {
|
||||
if (self._controller._flush_fn) |flush_fn| {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
try ls.toLocal(flush_fn).call(void, .{self._controller});
|
||||
}
|
||||
|
||||
try self._readable._controller.close();
|
||||
}
|
||||
|
||||
pub fn getReadable(self: *const TransformStream) *ReadableStream {
|
||||
return self._readable;
|
||||
}
|
||||
|
||||
pub fn getWritable(self: *const TransformStream) *WritableStream {
|
||||
return self._writable;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(TransformStream);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "TransformStream";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(TransformStream.init, .{});
|
||||
pub const readable = bridge.accessor(TransformStream.getReadable, null, .{});
|
||||
pub const writable = bridge.accessor(TransformStream.getWritable, null, .{});
|
||||
};
|
||||
|
||||
pub fn registerTypes() []const type {
|
||||
return &.{
|
||||
TransformStream,
|
||||
TransformStreamDefaultController,
|
||||
};
|
||||
}
|
||||
|
||||
pub const TransformStreamDefaultController = struct {
|
||||
_stream: *TransformStream,
|
||||
_transform_fn: ?js.Function.Global,
|
||||
_flush_fn: ?js.Function.Global,
|
||||
_zig_transform_fn: ?ZigTransformFn,
|
||||
|
||||
pub fn init(
|
||||
stream: *TransformStream,
|
||||
transform_fn: ?js.Function.Global,
|
||||
flush_fn: ?js.Function.Global,
|
||||
zig_transform_fn: ?ZigTransformFn,
|
||||
page: *Page,
|
||||
) !*TransformStreamDefaultController {
|
||||
return page._factory.create(TransformStreamDefaultController{
|
||||
._stream = stream,
|
||||
._transform_fn = transform_fn,
|
||||
._flush_fn = flush_fn,
|
||||
._zig_transform_fn = zig_transform_fn,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn enqueue(self: *TransformStreamDefaultController, chunk: ReadableStreamDefaultController.Chunk) !void {
|
||||
try self._stream._readable._controller.enqueue(chunk);
|
||||
}
|
||||
|
||||
/// Enqueue a raw JS value, preserving its type. Used by the JS-facing API.
|
||||
pub fn enqueueValue(self: *TransformStreamDefaultController, value: js.Value) !void {
|
||||
try self._stream._readable._controller.enqueueValue(value);
|
||||
}
|
||||
|
||||
pub fn doError(self: *TransformStreamDefaultController, reason: []const u8) !void {
|
||||
try self._stream._readable._controller.doError(reason);
|
||||
}
|
||||
|
||||
pub fn terminate(self: *TransformStreamDefaultController) !void {
|
||||
try self._stream._readable._controller.close();
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(TransformStreamDefaultController);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "TransformStreamDefaultController";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const enqueue = bridge.function(TransformStreamDefaultController.enqueueValue, .{});
|
||||
pub const @"error" = bridge.function(TransformStreamDefaultController.doError, .{});
|
||||
pub const terminate = bridge.function(TransformStreamDefaultController.terminate, .{});
|
||||
};
|
||||
};
|
||||
156
src/browser/webapi/streams/WritableStream.zig
Normal file
156
src/browser/webapi/streams/WritableStream.zig
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright (C) 2023-2026 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 js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
const WritableStreamDefaultWriter = @import("WritableStreamDefaultWriter.zig");
|
||||
const WritableStreamDefaultController = @import("WritableStreamDefaultController.zig");
|
||||
const TransformStream = @import("TransformStream.zig");
|
||||
|
||||
const WritableStream = @This();
|
||||
|
||||
pub const State = enum {
|
||||
writable,
|
||||
closed,
|
||||
errored,
|
||||
};
|
||||
|
||||
_state: State,
|
||||
_writer: ?*WritableStreamDefaultWriter,
|
||||
_controller: *WritableStreamDefaultController,
|
||||
_stored_error: ?[]const u8,
|
||||
_write_fn: ?js.Function.Global,
|
||||
_close_fn: ?js.Function.Global,
|
||||
_transform_stream: ?*TransformStream,
|
||||
|
||||
const UnderlyingSink = struct {
|
||||
start: ?js.Function = null,
|
||||
write: ?js.Function.Global = null,
|
||||
close: ?js.Function.Global = null,
|
||||
abort: ?js.Function.Global = null,
|
||||
type: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub fn init(sink_: ?UnderlyingSink, page: *Page) !*WritableStream {
|
||||
const self = try page._factory.create(WritableStream{
|
||||
._state = .writable,
|
||||
._writer = null,
|
||||
._controller = undefined,
|
||||
._stored_error = null,
|
||||
._write_fn = null,
|
||||
._close_fn = null,
|
||||
._transform_stream = null,
|
||||
});
|
||||
|
||||
self._controller = try WritableStreamDefaultController.init(self, page);
|
||||
|
||||
if (sink_) |sink| {
|
||||
if (sink.start) |start| {
|
||||
try start.call(void, .{self._controller});
|
||||
}
|
||||
self._write_fn = sink.write;
|
||||
self._close_fn = sink.close;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn initForTransform(transform_stream: *TransformStream, page: *Page) !*WritableStream {
|
||||
const self = try page._factory.create(WritableStream{
|
||||
._state = .writable,
|
||||
._writer = null,
|
||||
._controller = undefined,
|
||||
._stored_error = null,
|
||||
._write_fn = null,
|
||||
._close_fn = null,
|
||||
._transform_stream = transform_stream,
|
||||
});
|
||||
|
||||
self._controller = try WritableStreamDefaultController.init(self, page);
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn getWriter(self: *WritableStream, page: *Page) !*WritableStreamDefaultWriter {
|
||||
if (self.getLocked()) {
|
||||
return error.WriterLocked;
|
||||
}
|
||||
|
||||
const writer = try WritableStreamDefaultWriter.init(self, page);
|
||||
self._writer = writer;
|
||||
return writer;
|
||||
}
|
||||
|
||||
pub fn getLocked(self: *const WritableStream) bool {
|
||||
return self._writer != null;
|
||||
}
|
||||
|
||||
pub fn writeChunk(self: *WritableStream, chunk: js.Value, page: *Page) !void {
|
||||
if (self._state != .writable) return;
|
||||
|
||||
if (self._transform_stream) |ts| {
|
||||
try ts.transformWrite(chunk, page);
|
||||
return;
|
||||
}
|
||||
|
||||
if (self._write_fn) |write_fn| {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
try ls.toLocal(write_fn).call(void, .{ chunk, self._controller });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn closeStream(self: *WritableStream, page: *Page) !void {
|
||||
if (self._state != .writable) return;
|
||||
self._state = .closed;
|
||||
|
||||
if (self._transform_stream) |ts| {
|
||||
try ts.transformClose(page);
|
||||
return;
|
||||
}
|
||||
|
||||
if (self._close_fn) |close_fn| {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
try ls.toLocal(close_fn).call(void, .{self._controller});
|
||||
}
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(WritableStream);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "WritableStream";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(WritableStream.init, .{});
|
||||
pub const getWriter = bridge.function(WritableStream.getWriter, .{});
|
||||
pub const locked = bridge.accessor(WritableStream.getLocked, null, .{});
|
||||
};
|
||||
|
||||
pub fn registerTypes() []const type {
|
||||
return &.{
|
||||
WritableStream,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (C) 2023-2026 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 js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const WritableStream = @import("WritableStream.zig");
|
||||
|
||||
const WritableStreamDefaultController = @This();
|
||||
|
||||
_stream: *WritableStream,
|
||||
|
||||
pub fn init(stream: *WritableStream, page: *Page) !*WritableStreamDefaultController {
|
||||
return page._factory.create(WritableStreamDefaultController{
|
||||
._stream = stream,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn doError(self: *WritableStreamDefaultController, reason: []const u8) void {
|
||||
if (self._stream._state != .writable) return;
|
||||
self._stream._state = .errored;
|
||||
self._stream._stored_error = reason;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(WritableStreamDefaultController);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "WritableStreamDefaultController";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const @"error" = bridge.function(WritableStreamDefaultController.doError, .{});
|
||||
};
|
||||
109
src/browser/webapi/streams/WritableStreamDefaultWriter.zig
Normal file
109
src/browser/webapi/streams/WritableStreamDefaultWriter.zig
Normal file
@@ -0,0 +1,109 @@
|
||||
// Copyright (C) 2023-2026 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 js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const WritableStream = @import("WritableStream.zig");
|
||||
|
||||
const WritableStreamDefaultWriter = @This();
|
||||
|
||||
_stream: ?*WritableStream,
|
||||
|
||||
pub fn init(stream: *WritableStream, page: *Page) !*WritableStreamDefaultWriter {
|
||||
return page._factory.create(WritableStreamDefaultWriter{
|
||||
._stream = stream,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn write(self: *WritableStreamDefaultWriter, chunk: js.Value, page: *Page) !js.Promise {
|
||||
const stream = self._stream orelse {
|
||||
return page.js.local.?.rejectPromise("Writer has been released");
|
||||
};
|
||||
|
||||
if (stream._state != .writable) {
|
||||
return page.js.local.?.rejectPromise("Stream is not writable");
|
||||
}
|
||||
|
||||
try stream.writeChunk(chunk, page);
|
||||
|
||||
return page.js.local.?.resolvePromise(.{});
|
||||
}
|
||||
|
||||
pub fn close(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise {
|
||||
const stream = self._stream orelse {
|
||||
return page.js.local.?.rejectPromise("Writer has been released");
|
||||
};
|
||||
|
||||
if (stream._state != .writable) {
|
||||
return page.js.local.?.rejectPromise("Stream is not writable");
|
||||
}
|
||||
|
||||
try stream.closeStream(page);
|
||||
|
||||
return page.js.local.?.resolvePromise(.{});
|
||||
}
|
||||
|
||||
pub fn releaseLock(self: *WritableStreamDefaultWriter) void {
|
||||
if (self._stream) |stream| {
|
||||
stream._writer = null;
|
||||
self._stream = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getClosed(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise {
|
||||
const stream = self._stream orelse {
|
||||
return page.js.local.?.rejectPromise("Writer has been released");
|
||||
};
|
||||
|
||||
if (stream._state == .closed) {
|
||||
return page.js.local.?.resolvePromise(.{});
|
||||
}
|
||||
|
||||
return page.js.local.?.resolvePromise(.{});
|
||||
}
|
||||
|
||||
pub fn getDesiredSize(self: *const WritableStreamDefaultWriter) ?i32 {
|
||||
const stream = self._stream orelse return null;
|
||||
return switch (stream._state) {
|
||||
.writable => 1,
|
||||
.closed => 0,
|
||||
.errored => null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getReady(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise {
|
||||
_ = self;
|
||||
return page.js.local.?.resolvePromise(.{});
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(WritableStreamDefaultWriter);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "WritableStreamDefaultWriter";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const write = bridge.function(WritableStreamDefaultWriter.write, .{});
|
||||
pub const close = bridge.function(WritableStreamDefaultWriter.close, .{});
|
||||
pub const releaseLock = bridge.function(WritableStreamDefaultWriter.releaseLock, .{});
|
||||
pub const closed = bridge.accessor(WritableStreamDefaultWriter.getClosed, null, .{});
|
||||
pub const ready = bridge.accessor(WritableStreamDefaultWriter.getReady, null, .{});
|
||||
pub const desiredSize = bridge.accessor(WritableStreamDefaultWriter.getDesiredSize, null, .{});
|
||||
};
|
||||
@@ -406,10 +406,10 @@ pub fn requestAuthRequired(bc: anytype, intercept: *const Notification.RequestAu
|
||||
.fetch => "Fetch",
|
||||
},
|
||||
.authChallenge = .{
|
||||
.source = if (challenge.source == .server) "Server" else "Proxy",
|
||||
.origin = "", // TODO get origin, could be the proxy address for example.
|
||||
.scheme = if (challenge.scheme == .digest) "digest" else "basic",
|
||||
.realm = challenge.realm,
|
||||
.source = if (challenge.source) |s| (if (s == .server) "Server" else "Proxy") else "",
|
||||
.scheme = if (challenge.scheme) |s| (if (s == .digest) "digest" else "basic") else "",
|
||||
.realm = challenge.realm orelse "",
|
||||
},
|
||||
.networkId = &id.toRequestId(transfer.id),
|
||||
}, .{ .session_id = session_id });
|
||||
|
||||
@@ -379,13 +379,22 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
|
||||
}, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
// When we actually recreated the context we should have the inspector send this event, see: resetContextGroup
|
||||
// Sending this event will tell the client that the context ids they had are invalid and the context shouls be dropped
|
||||
// The client will expect us to send new contextCreated events, such that the client has new id's for the active contexts.
|
||||
try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
|
||||
// When we actually recreated the context we should have the inspector send
|
||||
// this event, see: resetContextGroup Sending this event will tell the
|
||||
// client that the context ids they had are invalid and the context shouls
|
||||
// be dropped The client will expect us to send new contextCreated events,
|
||||
// such that the client has new id's for the active contexts.
|
||||
// Only send executionContextsCleared for main frame navigations. For child
|
||||
// frames (iframes), clearing all contexts would destroy the main frame's
|
||||
// context, causing Puppeteer's page.evaluate()/page.content() to hang
|
||||
// forever.
|
||||
if (event.frame_id == page._frame_id) {
|
||||
try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
{
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
const aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\",\"loaderId\":\"{s}\"}}", .{ frame_id, loader_id });
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
|
||||
@@ -31,6 +31,7 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
getTargets,
|
||||
attachToTarget,
|
||||
attachToBrowserTarget,
|
||||
closeTarget,
|
||||
createBrowserContext,
|
||||
createTarget,
|
||||
@@ -47,6 +48,7 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
switch (action) {
|
||||
.getTargets => return getTargets(cmd),
|
||||
.attachToTarget => return attachToTarget(cmd),
|
||||
.attachToBrowserTarget => return attachToBrowserTarget(cmd),
|
||||
.closeTarget => return closeTarget(cmd),
|
||||
.createBrowserContext => return createBrowserContext(cmd),
|
||||
.createTarget => return createTarget(cmd),
|
||||
@@ -79,7 +81,7 @@ fn getTargets(cmd: anytype) !void {
|
||||
.targetInfos = [_]TargetInfo{.{
|
||||
.targetId = target_id,
|
||||
.type = "page",
|
||||
.title = bc.getTitle() orelse "about:blank",
|
||||
.title = bc.getTitle() orelse "",
|
||||
.url = bc.getURL() orelse "about:blank",
|
||||
.attached = true,
|
||||
.canAccessOpener = false,
|
||||
@@ -207,7 +209,7 @@ fn createTarget(cmd: anytype) !void {
|
||||
.targetInfo = TargetInfo{
|
||||
.attached = false,
|
||||
.targetId = target_id,
|
||||
.title = "about:blank",
|
||||
.title = "",
|
||||
.browserContextId = bc.id,
|
||||
.url = "about:blank",
|
||||
},
|
||||
@@ -243,14 +245,31 @@ fn attachToTarget(cmd: anytype) !void {
|
||||
return error.UnknownTargetId;
|
||||
}
|
||||
|
||||
if (bc.session_id == null) {
|
||||
try doAttachtoTarget(cmd, target_id);
|
||||
}
|
||||
try doAttachtoTarget(cmd, target_id);
|
||||
|
||||
return cmd.sendResult(
|
||||
.{ .sessionId = bc.session_id },
|
||||
.{ .include_session_id = false },
|
||||
);
|
||||
return cmd.sendResult(.{ .sessionId = bc.session_id }, .{});
|
||||
}
|
||||
|
||||
fn attachToBrowserTarget(cmd: anytype) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
const session_id = bc.session_id orelse cmd.cdp.session_id_gen.next();
|
||||
|
||||
try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{
|
||||
.sessionId = session_id,
|
||||
.targetInfo = TargetInfo{
|
||||
.targetId = bc.id, // We use the browser context is as browser's target id.
|
||||
.title = "",
|
||||
.url = "",
|
||||
.type = "browser",
|
||||
// Chrome doesn't send a browserContextId in this case.
|
||||
.browserContextId = null,
|
||||
},
|
||||
}, .{});
|
||||
|
||||
bc.session_id = session_id;
|
||||
|
||||
return cmd.sendResult(.{ .sessionId = bc.session_id }, .{});
|
||||
}
|
||||
|
||||
fn closeTarget(cmd: anytype) !void {
|
||||
@@ -311,7 +330,7 @@ fn getTargetInfo(cmd: anytype) !void {
|
||||
.targetInfo = TargetInfo{
|
||||
.targetId = target_id,
|
||||
.type = "page",
|
||||
.title = bc.getTitle() orelse "about:blank",
|
||||
.title = bc.getTitle() orelse "",
|
||||
.url = bc.getURL() orelse "about:blank",
|
||||
.attached = true,
|
||||
.canAccessOpener = false,
|
||||
@@ -323,7 +342,7 @@ fn getTargetInfo(cmd: anytype) !void {
|
||||
.targetInfo = TargetInfo{
|
||||
.targetId = "TID-STARTUP-B",
|
||||
.type = "browser",
|
||||
.title = "about:blank",
|
||||
.title = "",
|
||||
.url = "about:blank",
|
||||
.attached = true,
|
||||
.canAccessOpener = false,
|
||||
@@ -442,8 +461,8 @@ fn setAutoAttach(cmd: anytype) !void {
|
||||
.targetInfo = TargetInfo{
|
||||
.type = "page",
|
||||
.targetId = "TID-STARTUP-P",
|
||||
.title = "New Private Tab",
|
||||
.url = "chrome://newtab/",
|
||||
.title = "",
|
||||
.url = "about:blank",
|
||||
.browserContextId = "BID-STARTUP",
|
||||
},
|
||||
}, .{});
|
||||
@@ -451,22 +470,23 @@ fn setAutoAttach(cmd: anytype) !void {
|
||||
|
||||
fn doAttachtoTarget(cmd: anytype, target_id: []const u8) !void {
|
||||
const bc = cmd.browser_context.?;
|
||||
lp.assert(bc.session_id == null, "CDP.target.doAttachtoTarget not null session_id", .{});
|
||||
const session_id = cmd.cdp.session_id_gen.next();
|
||||
const session_id = bc.session_id orelse cmd.cdp.session_id_gen.next();
|
||||
|
||||
// extra_headers should not be kept on a new page or tab,
|
||||
// currently we have only 1 page, we clear it just in case
|
||||
bc.extra_headers.clearRetainingCapacity();
|
||||
if (bc.session_id == null) {
|
||||
// extra_headers should not be kept on a new page or tab,
|
||||
// currently we have only 1 page, we clear it just in case
|
||||
bc.extra_headers.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{
|
||||
.sessionId = session_id,
|
||||
.targetInfo = TargetInfo{
|
||||
.targetId = target_id,
|
||||
.title = "about:blank",
|
||||
.url = "chrome://newtab/",
|
||||
.title = bc.getTitle() orelse "",
|
||||
.url = bc.getURL() orelse "about:blank",
|
||||
.browserContextId = bc.id,
|
||||
},
|
||||
}, .{});
|
||||
}, .{ .session_id = bc.session_id });
|
||||
|
||||
bc.session_id = session_id;
|
||||
}
|
||||
@@ -568,7 +588,7 @@ test "cdp.target: createTarget" {
|
||||
|
||||
// should create a browser context
|
||||
const bc = ctx.cdp().browser_context.?;
|
||||
try ctx.expectSentEvent("Target.targetCreated", .{ .targetInfo = .{ .url = "about:blank", .title = "about:blank", .attached = false, .type = "page", .canAccessOpener = false, .browserContextId = bc.id, .targetId = bc.target_id.? } }, .{});
|
||||
try ctx.expectSentEvent("Target.targetCreated", .{ .targetInfo = .{ .url = "about:blank", .title = "", .attached = false, .type = "page", .canAccessOpener = false, .browserContextId = bc.id, .targetId = bc.target_id.? } }, .{});
|
||||
}
|
||||
|
||||
{
|
||||
@@ -580,8 +600,8 @@ test "cdp.target: createTarget" {
|
||||
|
||||
// should create a browser context
|
||||
const bc = ctx.cdp().browser_context.?;
|
||||
try ctx.expectSentEvent("Target.targetCreated", .{ .targetInfo = .{ .url = "about:blank", .title = "about:blank", .attached = false, .type = "page", .canAccessOpener = false, .browserContextId = bc.id, .targetId = bc.target_id.? } }, .{});
|
||||
try ctx.expectSentEvent("Target.attachedToTarget", .{ .sessionId = bc.session_id.?, .targetInfo = .{ .url = "chrome://newtab/", .title = "about:blank", .attached = true, .type = "page", .canAccessOpener = false, .browserContextId = bc.id, .targetId = bc.target_id.? } }, .{});
|
||||
try ctx.expectSentEvent("Target.targetCreated", .{ .targetInfo = .{ .url = "about:blank", .title = "", .attached = false, .type = "page", .canAccessOpener = false, .browserContextId = bc.id, .targetId = bc.target_id.? } }, .{});
|
||||
try ctx.expectSentEvent("Target.attachedToTarget", .{ .sessionId = bc.session_id.?, .targetInfo = .{ .url = "about:blank", .title = "", .attached = true, .type = "page", .canAccessOpener = false, .browserContextId = bc.id, .targetId = bc.target_id.? } }, .{});
|
||||
}
|
||||
|
||||
var ctx = testing.context();
|
||||
@@ -596,7 +616,7 @@ test "cdp.target: createTarget" {
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
|
||||
try testing.expectEqual(true, bc.target_id != null);
|
||||
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
|
||||
try ctx.expectSentEvent("Target.targetCreated", .{ .targetInfo = .{ .url = "about:blank", .title = "about:blank", .attached = false, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{});
|
||||
try ctx.expectSentEvent("Target.targetCreated", .{ .targetInfo = .{ .url = "about:blank", .title = "", .attached = false, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -658,7 +678,7 @@ test "cdp.target: attachToTarget" {
|
||||
try ctx.processMessage(.{ .id = 11, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-000000000B" } });
|
||||
const session_id = bc.session_id.?;
|
||||
try ctx.expectSentResult(.{ .sessionId = session_id }, .{ .id = 11 });
|
||||
try ctx.expectSentEvent("Target.attachedToTarget", .{ .sessionId = session_id, .targetInfo = .{ .url = "chrome://newtab/", .title = "about:blank", .attached = true, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{});
|
||||
try ctx.expectSentEvent("Target.attachedToTarget", .{ .sessionId = session_id, .targetInfo = .{ .url = "about:blank", .title = "", .attached = true, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,7 +691,7 @@ test "cdp.target: getTargetInfo" {
|
||||
try ctx.expectSentResult(.{
|
||||
.targetInfo = .{
|
||||
.type = "browser",
|
||||
.title = "about:blank",
|
||||
.title = "",
|
||||
.url = "about:blank",
|
||||
.attached = true,
|
||||
.canAccessOpener = false,
|
||||
|
||||
@@ -46,7 +46,7 @@ pub fn toLoaderId(page_id: u32) [14]u8 {
|
||||
|
||||
pub fn toRequestId(page_id: u32) [14]u8 {
|
||||
var buf: [14]u8 = undefined;
|
||||
_ = std.fmt.bufPrint(&buf, "RID-{d:0>10}", .{page_id}) catch unreachable;
|
||||
_ = std.fmt.bufPrint(&buf, "REQ-{d:0>10}", .{page_id}) catch unreachable;
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -174,8 +174,8 @@ test "id: toLoaderId" {
|
||||
}
|
||||
|
||||
test "id: toRequestId" {
|
||||
try testing.expectEqual("RID-0000000000", toRequestId(0));
|
||||
try testing.expectEqual("RID-4294967295", toRequestId(4294967295));
|
||||
try testing.expectEqual("REQ-0000000000", toRequestId(0));
|
||||
try testing.expectEqual("REQ-4294967295", toRequestId(4294967295));
|
||||
}
|
||||
|
||||
test "id: toInterceptId" {
|
||||
|
||||
@@ -66,7 +66,7 @@ active: usize,
|
||||
intercepted: usize,
|
||||
|
||||
// Our easy handles, managed by a curl multi.
|
||||
handles: Handles,
|
||||
handles: Net.Handles,
|
||||
|
||||
// Use to generate the next request ID
|
||||
next_request_id: u32 = 0,
|
||||
@@ -128,7 +128,7 @@ pub fn init(allocator: Allocator, ca_blob: ?Net.Blob, robot_store: *RobotStore,
|
||||
const client = try allocator.create(Client);
|
||||
errdefer allocator.destroy(client);
|
||||
|
||||
var handles = try Handles.init(allocator, ca_blob, config);
|
||||
var handles = try Net.Handles.init(allocator, ca_blob, config);
|
||||
errdefer handles.deinit(allocator);
|
||||
|
||||
// Set transfer callbacks on each connection.
|
||||
@@ -173,6 +173,14 @@ pub fn newHeaders(self: *const Client) !Net.Headers {
|
||||
return Net.Headers.init(self.config.http_headers.user_agent_header);
|
||||
}
|
||||
|
||||
pub fn attachRuntime(self: *Client, runtime: *Net.Runtime, cdp_fd: ?posix.fd_t) !void {
|
||||
try self.handles.attachRuntime(runtime, cdp_fd);
|
||||
}
|
||||
|
||||
pub fn detachRuntime(self: *Client) void {
|
||||
self.handles.detachRuntime();
|
||||
}
|
||||
|
||||
pub fn abort(self: *Client) void {
|
||||
self._abort(true, 0);
|
||||
}
|
||||
@@ -191,6 +199,8 @@ fn _abort(self: *Client, comptime abort_all: bool, frame_id: u32) void {
|
||||
n = node.next;
|
||||
const conn: *Net.Connection = @fieldParentPtr("node", node);
|
||||
var transfer = Transfer.fromConnection(conn) catch |err| {
|
||||
// Let's cleanup what we can
|
||||
self.handles.remove(conn);
|
||||
log.err(.http, "get private info", .{ .err = err, .source = "abort" });
|
||||
continue;
|
||||
};
|
||||
@@ -665,7 +675,7 @@ pub fn restoreOriginalProxy(self: *Client) !void {
|
||||
}
|
||||
|
||||
// Enable TLS verification on all connections.
|
||||
pub fn enableTlsVerify(self: *const Client) !void {
|
||||
pub fn enableTlsVerify(self: *Client) !void {
|
||||
// Remove inflight connections check on enable TLS b/c chromiumoxide calls
|
||||
// the command during navigate and Curl seems to accept it...
|
||||
|
||||
@@ -675,7 +685,7 @@ pub fn enableTlsVerify(self: *const Client) !void {
|
||||
}
|
||||
|
||||
// Disable TLS verification on all connections.
|
||||
pub fn disableTlsVerify(self: *const Client) !void {
|
||||
pub fn disableTlsVerify(self: *Client) !void {
|
||||
// Remove inflight connections check on disable TLS b/c chromiumoxide calls
|
||||
// the command during navigate and Curl seems to accept it...
|
||||
|
||||
@@ -689,7 +699,11 @@ fn makeRequest(self: *Client, conn: *Net.Connection, transfer: *Transfer) anyerr
|
||||
|
||||
{
|
||||
transfer._conn = conn;
|
||||
errdefer transfer.deinit();
|
||||
errdefer {
|
||||
transfer._conn = null;
|
||||
transfer.deinit();
|
||||
self.handles.isAvailable(conn);
|
||||
}
|
||||
|
||||
try conn.setURL(req.url);
|
||||
try conn.setMethod(req.method);
|
||||
@@ -716,17 +730,20 @@ fn makeRequest(self: *Client, conn: *Net.Connection, transfer: *Transfer) anyerr
|
||||
}
|
||||
}
|
||||
|
||||
// Once soon as this is called, our "perform" loop is responsible for
|
||||
// As soon as this is called, our "perform" loop is responsible for
|
||||
// cleaning things up. That's why the above code is in a block. If anything
|
||||
// fails BEFORE `curl_multi_add_handle` suceeds, the we still need to do
|
||||
// fails BEFORE `curl_multi_add_handle` succeeds, the we still need to do
|
||||
// cleanup. But if things fail after `curl_multi_add_handle`, we expect
|
||||
// perfom to pickup the failure and cleanup.
|
||||
try self.handles.add(conn);
|
||||
self.handles.add(conn) catch |err| {
|
||||
transfer._conn = null;
|
||||
transfer.deinit();
|
||||
self.handles.isAvailable(conn);
|
||||
return err;
|
||||
};
|
||||
|
||||
if (req.start_callback) |cb| {
|
||||
cb(transfer) catch |err| {
|
||||
self.handles.remove(conn);
|
||||
transfer._conn = null;
|
||||
transfer.deinit();
|
||||
return err;
|
||||
};
|
||||
@@ -834,7 +851,7 @@ fn processMessages(self: *Client) !bool {
|
||||
// In case of request w/o data, we need to call the header done
|
||||
// callback now.
|
||||
const proceed = transfer.headerDoneCallback(&msg.conn) catch |err| {
|
||||
log.err(.http, "header_done_callback", .{ .err = err });
|
||||
log.err(.http, "header_done_callback2", .{ .err = err });
|
||||
requestFailed(transfer, err, true);
|
||||
continue;
|
||||
};
|
||||
@@ -872,8 +889,6 @@ fn ensureNoActiveConnection(self: *const Client) !void {
|
||||
}
|
||||
}
|
||||
|
||||
const Handles = Net.Handles;
|
||||
|
||||
pub const RequestCookie = struct {
|
||||
is_http: bool,
|
||||
jar: *CookieJar,
|
||||
@@ -1300,9 +1315,9 @@ pub const Transfer = struct {
|
||||
// WWW-Authenticate or Proxy-Authenticate header.
|
||||
transfer._auth_challenge = .{
|
||||
.status = status,
|
||||
.source = undefined,
|
||||
.scheme = undefined,
|
||||
.realm = undefined,
|
||||
.source = null,
|
||||
.scheme = null,
|
||||
.realm = null,
|
||||
};
|
||||
return buf_len;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,32 @@ pub const CurlOffT = c.curl_off_t;
|
||||
pub const CurlDebugFunction = fn (*Curl, CurlInfoType, [*c]u8, usize, *anyopaque) c_int;
|
||||
pub const CurlHeaderFunction = fn ([*]const u8, usize, usize, *anyopaque) usize;
|
||||
pub const CurlWriteFunction = fn ([*]const u8, usize, usize, *anyopaque) usize;
|
||||
pub const CurlSocketCallback = fn (?*Curl, CurlSocket, c_int, ?*anyopaque, ?*anyopaque) callconv(.c) c_int;
|
||||
pub const CurlTimerCallback = fn (?*CurlM, c_long, ?*anyopaque) callconv(.c) c_int;
|
||||
pub const curl_writefunc_error: usize = c.CURL_WRITEFUNC_ERROR;
|
||||
pub const CURL_SOCKET_TIMEOUT: CurlSocket = c.CURL_SOCKET_TIMEOUT;
|
||||
|
||||
pub const CurlPoll = enum(c_int) {
|
||||
in = c.CURL_POLL_IN,
|
||||
out = c.CURL_POLL_OUT,
|
||||
inout = c.CURL_POLL_INOUT,
|
||||
remove = c.CURL_POLL_REMOVE,
|
||||
};
|
||||
|
||||
pub const CurlSelectMask = packed struct(c_int) {
|
||||
in: bool = false,
|
||||
out: bool = false,
|
||||
err: bool = false,
|
||||
_reserved: std.meta.Int(.unsigned, @bitSizeOf(c_int) - 3) = 0,
|
||||
|
||||
pub fn toC(self: @This()) c_int {
|
||||
var mask: c_int = 0;
|
||||
if (self.in) mask |= c.CURL_CSELECT_IN;
|
||||
if (self.out) mask |= c.CURL_CSELECT_OUT;
|
||||
if (self.err) mask |= c.CURL_CSELECT_ERR;
|
||||
return mask;
|
||||
}
|
||||
};
|
||||
|
||||
pub const CurlGlobalFlags = packed struct(u8) {
|
||||
ssl: bool = false,
|
||||
@@ -156,6 +181,10 @@ pub const CurlOption = enum(c.CURLoption) {
|
||||
|
||||
pub const CurlMOption = enum(c.CURLMoption) {
|
||||
max_host_connections = c.CURLMOPT_MAX_HOST_CONNECTIONS,
|
||||
socket_function = c.CURLMOPT_SOCKETFUNCTION,
|
||||
socket_data = c.CURLMOPT_SOCKETDATA,
|
||||
timer_function = c.CURLMOPT_TIMERFUNCTION,
|
||||
timer_data = c.CURLMOPT_TIMERDATA,
|
||||
};
|
||||
|
||||
pub const CurlInfo = enum(c.CURLINFO) {
|
||||
@@ -675,6 +704,10 @@ pub fn curl_multi_setopt(multi: *CurlM, comptime option: CurlMOption, value: any
|
||||
};
|
||||
break :blk c.curl_multi_setopt(multi, opt, n);
|
||||
},
|
||||
.socket_function => c.curl_multi_setopt(multi, opt, value),
|
||||
.socket_data => c.curl_multi_setopt(multi, opt, value),
|
||||
.timer_function => c.curl_multi_setopt(multi, opt, value),
|
||||
.timer_data => c.curl_multi_setopt(multi, opt, value),
|
||||
};
|
||||
try errorMCheck(code);
|
||||
}
|
||||
@@ -701,6 +734,15 @@ pub fn curl_multi_poll(
|
||||
try errorMCheck(c.curl_multi_poll(multi, raw_fds, @intCast(extra_fds.len), timeout_ms, numfds));
|
||||
}
|
||||
|
||||
pub fn curl_multi_socket_action(
|
||||
multi: *CurlM,
|
||||
s: CurlSocket,
|
||||
ev_bitmask: c_int,
|
||||
running_handles: *c_int,
|
||||
) ErrorMulti!void {
|
||||
try errorMCheck(c.curl_multi_socket_action(multi, s, ev_bitmask, running_handles));
|
||||
}
|
||||
|
||||
pub fn curl_multi_info_read(multi: *CurlM, msgs_in_queue: *c_int) ?CurlMsg {
|
||||
const ptr = c.curl_multi_info_read(multi, msgs_in_queue);
|
||||
if (ptr == null) return null;
|
||||
|
||||
Reference in New Issue
Block a user