mirror of
				https://github.com/lightpanda-io/browser.git
				synced 2025-10-29 23:23:28 +00:00 
			
		
		
		
	async: remove pseudo-async http client
This commit is contained in:
		
							
								
								
									
										1766
									
								
								src/async/Client.zig
									
									
									
									
									
								
							
							
						
						
									
										1766
									
								
								src/async/Client.zig
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,133 +0,0 @@ | |||||||
| // Copyright (C) 2023-2024  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 builtin = @import("builtin"); |  | ||||||
| const posix = std.posix; |  | ||||||
| const io = std.io; |  | ||||||
| const assert = std.debug.assert; |  | ||||||
|  |  | ||||||
| const tcp = @import("tcp.zig"); |  | ||||||
|  |  | ||||||
| pub const Stream = struct { |  | ||||||
|     alloc: std.mem.Allocator, |  | ||||||
|     conn: *tcp.Conn, |  | ||||||
|  |  | ||||||
|     handle: posix.socket_t, |  | ||||||
|  |  | ||||||
|     pub fn close(self: Stream) void { |  | ||||||
|         posix.close(self.handle); |  | ||||||
|         self.alloc.destroy(self.conn); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pub const ReadError = posix.ReadError; |  | ||||||
|     pub const WriteError = posix.WriteError; |  | ||||||
|  |  | ||||||
|     pub const Reader = io.Reader(Stream, ReadError, read); |  | ||||||
|     pub const Writer = io.Writer(Stream, WriteError, write); |  | ||||||
|  |  | ||||||
|     pub fn reader(self: Stream) Reader { |  | ||||||
|         return .{ .context = self }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pub fn writer(self: Stream) Writer { |  | ||||||
|         return .{ .context = self }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pub fn read(self: Stream, buffer: []u8) ReadError!usize { |  | ||||||
|         return self.conn.receive(self.handle, buffer) catch |err| switch (err) { |  | ||||||
|             else => return error.Unexpected, |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pub fn readv(s: Stream, iovecs: []const posix.iovec) ReadError!usize { |  | ||||||
|         return posix.readv(s.handle, iovecs); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /// Returns the number of bytes read. If the number read is smaller than |  | ||||||
|     /// `buffer.len`, it means the stream reached the end. Reaching the end of |  | ||||||
|     /// a stream is not an error condition. |  | ||||||
|     pub fn readAll(s: Stream, buffer: []u8) ReadError!usize { |  | ||||||
|         return readAtLeast(s, buffer, buffer.len); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /// Returns the number of bytes read, calling the underlying read function |  | ||||||
|     /// the minimal number of times until the buffer has at least `len` bytes |  | ||||||
|     /// filled. If the number read is less than `len` it means the stream |  | ||||||
|     /// reached the end. Reaching the end of the stream is not an error |  | ||||||
|     /// condition. |  | ||||||
|     pub fn readAtLeast(s: Stream, buffer: []u8, len: usize) ReadError!usize { |  | ||||||
|         assert(len <= buffer.len); |  | ||||||
|         var index: usize = 0; |  | ||||||
|         while (index < len) { |  | ||||||
|             const amt = try s.read(buffer[index..]); |  | ||||||
|             if (amt == 0) break; |  | ||||||
|             index += amt; |  | ||||||
|         } |  | ||||||
|         return index; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /// TODO in evented I/O mode, this implementation incorrectly uses the event loop's |  | ||||||
|     /// file system thread instead of non-blocking. It needs to be reworked to properly |  | ||||||
|     /// use non-blocking I/O. |  | ||||||
|     pub fn write(self: Stream, buffer: []const u8) WriteError!usize { |  | ||||||
|         return self.conn.send(self.handle, buffer) catch |err| switch (err) { |  | ||||||
|             error.AccessDenied => error.AccessDenied, |  | ||||||
|             error.WouldBlock => error.WouldBlock, |  | ||||||
|             error.ConnectionResetByPeer => error.ConnectionResetByPeer, |  | ||||||
|             error.MessageTooBig => error.FileTooBig, |  | ||||||
|             error.BrokenPipe => error.BrokenPipe, |  | ||||||
|             else => return error.Unexpected, |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pub fn writeAll(self: Stream, bytes: []const u8) WriteError!void { |  | ||||||
|         var index: usize = 0; |  | ||||||
|         while (index < bytes.len) { |  | ||||||
|             index += try self.write(bytes[index..]); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /// See https://github.com/ziglang/zig/issues/7699 |  | ||||||
|     /// See equivalent function: `std.fs.File.writev`. |  | ||||||
|     pub fn writev(self: Stream, iovecs: []const posix.iovec_const) WriteError!usize { |  | ||||||
|         if (iovecs.len == 0) return 0; |  | ||||||
|         const first_buffer = iovecs[0].base[0..iovecs[0].len]; |  | ||||||
|         return try self.write(first_buffer); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /// The `iovecs` parameter is mutable because this function needs to mutate the fields in |  | ||||||
|     /// order to handle partial writes from the underlying OS layer. |  | ||||||
|     /// See https://github.com/ziglang/zig/issues/7699 |  | ||||||
|     /// See equivalent function: `std.fs.File.writevAll`. |  | ||||||
|     pub fn writevAll(self: Stream, iovecs: []posix.iovec_const) WriteError!void { |  | ||||||
|         if (iovecs.len == 0) return; |  | ||||||
|  |  | ||||||
|         var i: usize = 0; |  | ||||||
|         while (true) { |  | ||||||
|             var amt = try self.writev(iovecs[i..]); |  | ||||||
|             while (amt >= iovecs[i].len) { |  | ||||||
|                 amt -= iovecs[i].len; |  | ||||||
|                 i += 1; |  | ||||||
|                 if (i >= iovecs.len) return; |  | ||||||
|             } |  | ||||||
|             iovecs[i].base += amt; |  | ||||||
|             iovecs[i].len -= amt; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| }; |  | ||||||
| @@ -1,112 +0,0 @@ | |||||||
| // Copyright (C) 2023-2024  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 net = std.net; |  | ||||||
| const Stream = @import("stream.zig").Stream; |  | ||||||
| const Loop = @import("jsruntime").Loop; |  | ||||||
| const NetworkImpl = Loop.Network(Conn.Command); |  | ||||||
|  |  | ||||||
| // Conn is a TCP connection using jsruntime Loop async I/O. |  | ||||||
| // connect, send and receive are blocking, but use async I/O in the background. |  | ||||||
| // Client doesn't own the socket used for the connection, the caller is |  | ||||||
| // responsible for closing it. |  | ||||||
| pub const Conn = struct { |  | ||||||
|     const Command = struct { |  | ||||||
|         impl: NetworkImpl, |  | ||||||
|  |  | ||||||
|         done: bool = false, |  | ||||||
|         err: ?anyerror = null, |  | ||||||
|         ln: usize = 0, |  | ||||||
|  |  | ||||||
|         fn ok(self: *Command, err: ?anyerror, ln: usize) void { |  | ||||||
|             self.err = err; |  | ||||||
|             self.ln = ln; |  | ||||||
|             self.done = true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         fn wait(self: *Command) !usize { |  | ||||||
|             while (!self.done) try self.impl.tick(); |  | ||||||
|  |  | ||||||
|             if (self.err) |err| return err; |  | ||||||
|             return self.ln; |  | ||||||
|         } |  | ||||||
|         pub fn onConnect(self: *Command, err: ?anyerror) void { |  | ||||||
|             self.ok(err, 0); |  | ||||||
|         } |  | ||||||
|         pub fn onSend(self: *Command, ln: usize, err: ?anyerror) void { |  | ||||||
|             self.ok(err, ln); |  | ||||||
|         } |  | ||||||
|         pub fn onReceive(self: *Command, ln: usize, err: ?anyerror) void { |  | ||||||
|             self.ok(err, ln); |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     loop: *Loop, |  | ||||||
|  |  | ||||||
|     pub fn connect(self: *Conn, socket: std.posix.socket_t, address: std.net.Address) !void { |  | ||||||
|         var cmd = Command{ .impl = NetworkImpl.init(self.loop) }; |  | ||||||
|         cmd.impl.connect(&cmd, socket, address); |  | ||||||
|         _ = try cmd.wait(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pub fn send(self: *Conn, socket: std.posix.socket_t, buffer: []const u8) !usize { |  | ||||||
|         var cmd = Command{ .impl = NetworkImpl.init(self.loop) }; |  | ||||||
|         cmd.impl.send(&cmd, socket, buffer); |  | ||||||
|         return try cmd.wait(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pub fn receive(self: *Conn, socket: std.posix.socket_t, buffer: []u8) !usize { |  | ||||||
|         var cmd = Command{ .impl = NetworkImpl.init(self.loop) }; |  | ||||||
|         cmd.impl.receive(&cmd, socket, buffer); |  | ||||||
|         return try cmd.wait(); |  | ||||||
|     } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| pub fn tcpConnectToHost(alloc: std.mem.Allocator, loop: *Loop, name: []const u8, port: u16) !Stream { |  | ||||||
|     // TODO async resolve |  | ||||||
|     const list = try net.getAddressList(alloc, name, port); |  | ||||||
|     defer list.deinit(); |  | ||||||
|  |  | ||||||
|     if (list.addrs.len == 0) return error.UnknownHostName; |  | ||||||
|  |  | ||||||
|     for (list.addrs) |addr| { |  | ||||||
|         return tcpConnectToAddress(alloc, loop, addr) catch |err| switch (err) { |  | ||||||
|             error.ConnectionRefused => { |  | ||||||
|                 continue; |  | ||||||
|             }, |  | ||||||
|             else => return err, |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
|     return std.posix.ConnectError.ConnectionRefused; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub fn tcpConnectToAddress(alloc: std.mem.Allocator, loop: *Loop, addr: net.Address) !Stream { |  | ||||||
|     const sockfd = try std.posix.socket(addr.any.family, std.posix.SOCK.STREAM, std.posix.IPPROTO.TCP); |  | ||||||
|     errdefer std.posix.close(sockfd); |  | ||||||
|  |  | ||||||
|     var conn = try alloc.create(Conn); |  | ||||||
|     conn.* = Conn{ .loop = loop }; |  | ||||||
|     try conn.connect(sockfd, addr); |  | ||||||
|  |  | ||||||
|     return Stream{ |  | ||||||
|         .alloc = alloc, |  | ||||||
|         .conn = conn, |  | ||||||
|         .handle = sockfd, |  | ||||||
|     }; |  | ||||||
| } |  | ||||||
| @@ -1,189 +0,0 @@ | |||||||
| // Copyright (C) 2023-2024  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 http = std.http; |  | ||||||
| const Client = @import("Client.zig"); |  | ||||||
| const Request = @import("Client.zig").Request; |  | ||||||
|  |  | ||||||
| pub const Loop = @import("jsruntime").Loop; |  | ||||||
|  |  | ||||||
| const url = "https://w3.org"; |  | ||||||
|  |  | ||||||
| test "blocking mode fetch API" { |  | ||||||
|     const alloc = std.testing.allocator; |  | ||||||
|  |  | ||||||
|     var loop = try Loop.init(alloc); |  | ||||||
|     defer loop.deinit(); |  | ||||||
|  |  | ||||||
|     var client: Client = .{ |  | ||||||
|         .allocator = alloc, |  | ||||||
|         .loop = &loop, |  | ||||||
|     }; |  | ||||||
|     defer client.deinit(); |  | ||||||
|  |  | ||||||
|     // force client's CA cert scan from system. |  | ||||||
|     try client.ca_bundle.rescan(client.allocator); |  | ||||||
|  |  | ||||||
|     const res = try client.fetch(.{ |  | ||||||
|         .location = .{ .uri = try std.Uri.parse(url) }, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     try std.testing.expect(res.status == .ok); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| test "blocking mode open/send/wait API" { |  | ||||||
|     const alloc = std.testing.allocator; |  | ||||||
|  |  | ||||||
|     var loop = try Loop.init(alloc); |  | ||||||
|     defer loop.deinit(); |  | ||||||
|  |  | ||||||
|     var client: Client = .{ |  | ||||||
|         .allocator = alloc, |  | ||||||
|         .loop = &loop, |  | ||||||
|     }; |  | ||||||
|     defer client.deinit(); |  | ||||||
|  |  | ||||||
|     // force client's CA cert scan from system. |  | ||||||
|     try client.ca_bundle.rescan(client.allocator); |  | ||||||
|  |  | ||||||
|     var buf: [2014]u8 = undefined; |  | ||||||
|     var req = try client.open(.GET, try std.Uri.parse(url), .{ |  | ||||||
|         .server_header_buffer = &buf, |  | ||||||
|     }); |  | ||||||
|     defer req.deinit(); |  | ||||||
|  |  | ||||||
|     try req.send(); |  | ||||||
|     try req.finish(); |  | ||||||
|     try req.wait(); |  | ||||||
|  |  | ||||||
|     try std.testing.expect(req.response.status == .ok); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Example how to write an async http client using the modified standard client. |  | ||||||
| const AsyncClient = struct { |  | ||||||
|     cli: Client, |  | ||||||
|  |  | ||||||
|     const YieldImpl = Loop.Yield(AsyncRequest); |  | ||||||
|     const AsyncRequest = struct { |  | ||||||
|         const State = enum { new, open, send, finish, wait, done }; |  | ||||||
|  |  | ||||||
|         cli: *Client, |  | ||||||
|         uri: std.Uri, |  | ||||||
|  |  | ||||||
|         req: ?Request = undefined, |  | ||||||
|         state: State = .new, |  | ||||||
|  |  | ||||||
|         impl: YieldImpl, |  | ||||||
|         err: ?anyerror = null, |  | ||||||
|  |  | ||||||
|         buf: [2014]u8 = undefined, |  | ||||||
|  |  | ||||||
|         pub fn deinit(self: *AsyncRequest) void { |  | ||||||
|             if (self.req) |*r| r.deinit(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         pub fn fetch(self: *AsyncRequest) void { |  | ||||||
|             self.state = .new; |  | ||||||
|             return self.impl.yield(self); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         fn onerr(self: *AsyncRequest, err: anyerror) void { |  | ||||||
|             self.state = .done; |  | ||||||
|             self.err = err; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         pub fn onYield(self: *AsyncRequest, err: ?anyerror) void { |  | ||||||
|             if (err) |e| return self.onerr(e); |  | ||||||
|  |  | ||||||
|             switch (self.state) { |  | ||||||
|                 .new => { |  | ||||||
|                     self.state = .open; |  | ||||||
|                     self.req = self.cli.open(.GET, self.uri, .{ |  | ||||||
|                         .server_header_buffer = &self.buf, |  | ||||||
|                     }) catch |e| return self.onerr(e); |  | ||||||
|                 }, |  | ||||||
|                 .open => { |  | ||||||
|                     self.state = .send; |  | ||||||
|                     self.req.?.send() catch |e| return self.onerr(e); |  | ||||||
|                 }, |  | ||||||
|                 .send => { |  | ||||||
|                     self.state = .finish; |  | ||||||
|                     self.req.?.finish() catch |e| return self.onerr(e); |  | ||||||
|                 }, |  | ||||||
|                 .finish => { |  | ||||||
|                     self.state = .wait; |  | ||||||
|                     self.req.?.wait() catch |e| return self.onerr(e); |  | ||||||
|                 }, |  | ||||||
|                 .wait => { |  | ||||||
|                     self.state = .done; |  | ||||||
|                     return; |  | ||||||
|                 }, |  | ||||||
|                 .done => return, |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             return self.impl.yield(self); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         pub fn wait(self: *AsyncRequest) !void { |  | ||||||
|             while (self.state != .done) try self.impl.tick(); |  | ||||||
|             if (self.err) |err| return err; |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     pub fn init(alloc: std.mem.Allocator, loop: *Loop) AsyncClient { |  | ||||||
|         return .{ |  | ||||||
|             .cli = .{ |  | ||||||
|                 .allocator = alloc, |  | ||||||
|                 .loop = loop, |  | ||||||
|             }, |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pub fn deinit(self: *AsyncClient) void { |  | ||||||
|         self.cli.deinit(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pub fn createRequest(self: *AsyncClient, uri: std.Uri) !AsyncRequest { |  | ||||||
|         return .{ |  | ||||||
|             .impl = YieldImpl.init(self.cli.loop), |  | ||||||
|             .cli = &self.cli, |  | ||||||
|             .uri = uri, |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| test "non blocking client" { |  | ||||||
|     const alloc = std.testing.allocator; |  | ||||||
|  |  | ||||||
|     var loop = try Loop.init(alloc); |  | ||||||
|     defer loop.deinit(); |  | ||||||
|  |  | ||||||
|     var client = AsyncClient.init(alloc, &loop); |  | ||||||
|     defer client.deinit(); |  | ||||||
|  |  | ||||||
|     var reqs: [3]AsyncClient.AsyncRequest = undefined; |  | ||||||
|     for (0..reqs.len) |i| { |  | ||||||
|         reqs[i] = try client.createRequest(try std.Uri.parse(url)); |  | ||||||
|         reqs[i].fetch(); |  | ||||||
|     } |  | ||||||
|     for (0..reqs.len) |i| { |  | ||||||
|         try reqs[i].wait(); |  | ||||||
|         reqs[i].deinit(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -29,7 +29,7 @@ const html_test = @import("html_test.zig").html; | |||||||
|  |  | ||||||
| pub const Types = jsruntime.reflect(apiweb.Interfaces); | pub const Types = jsruntime.reflect(apiweb.Interfaces); | ||||||
| pub const UserContext = apiweb.UserContext; | pub const UserContext = apiweb.UserContext; | ||||||
| const Client = @import("async/Client.zig"); | const Client = @import("http/async/main.zig").Client; | ||||||
|  |  | ||||||
| var doc: *parser.DocumentHTML = undefined; | var doc: *parser.DocumentHTML = undefined; | ||||||
|  |  | ||||||
| @@ -41,7 +41,7 @@ fn execJS( | |||||||
|     try js_env.start(); |     try js_env.start(); | ||||||
|     defer js_env.stop(); |     defer js_env.stop(); | ||||||
|  |  | ||||||
|     var cli = Client{ .allocator = alloc, .loop = js_env.nat_ctx.loop }; |     var cli = Client{ .allocator = alloc }; | ||||||
|     defer cli.deinit(); |     defer cli.deinit(); | ||||||
|  |  | ||||||
|     try js_env.setUserContext(UserContext{ |     try js_env.setUserContext(UserContext{ | ||||||
|   | |||||||
| @@ -298,7 +298,7 @@ test { | |||||||
|     const msgTest = @import("msg.zig"); |     const msgTest = @import("msg.zig"); | ||||||
|     std.testing.refAllDecls(msgTest); |     std.testing.refAllDecls(msgTest); | ||||||
|  |  | ||||||
|     const asyncTest = @import("async/test.zig"); |     const asyncTest = @import("http/async/std/http.zig"); | ||||||
|     std.testing.refAllDecls(asyncTest); |     std.testing.refAllDecls(asyncTest); | ||||||
|  |  | ||||||
|     const dumpTest = @import("browser/dump.zig"); |     const dumpTest = @import("browser/dump.zig"); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Pierre Tachoire
					Pierre Tachoire