mirror of
				https://github.com/lightpanda-io/browser.git
				synced 2025-10-30 15:41:48 +00:00 
			
		
		
		
	initial fetch in zig
This commit is contained in:
		| @@ -36,8 +36,9 @@ const WebApis = struct { | ||||
|         @import("xhr/form_data.zig").Interfaces, | ||||
|         @import("xhr/File.zig"), | ||||
|         @import("xmlserializer/xmlserializer.zig").Interfaces, | ||||
|         @import("fetch/Request.zig"), | ||||
|         @import("fetch/Headers.zig"), | ||||
|         @import("fetch/Request.zig"), | ||||
|         @import("fetch/Response.zig"), | ||||
|     }); | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -20,18 +20,36 @@ const std = @import("std"); | ||||
| const URL = @import("../../url.zig").URL; | ||||
| const Page = @import("../page.zig").Page; | ||||
|  | ||||
| // https://developer.mozilla.org/en-US/docs/Web/API/Request/Request | ||||
| const Request = @This(); | ||||
| const Response = @import("./Response.zig"); | ||||
|  | ||||
| url: []const u8, | ||||
| const Http = @import("../../http/Http.zig"); | ||||
| const HttpClient = @import("../../http/Client.zig"); | ||||
| const Mime = @import("../mime.zig").Mime; | ||||
|  | ||||
| const RequestInput = union(enum) { | ||||
| const v8 = @import("v8"); | ||||
| const Env = @import("../env.zig").Env; | ||||
|  | ||||
| pub const RequestInput = union(enum) { | ||||
|     string: []const u8, | ||||
|     request: Request, | ||||
| }; | ||||
|  | ||||
| pub fn constructor(input: RequestInput, page: *Page) !Request { | ||||
| // https://developer.mozilla.org/en-US/docs/Web/API/RequestInit | ||||
| pub const RequestInit = struct { | ||||
|     method: []const u8 = "GET", | ||||
|     body: []const u8 = "", | ||||
| }; | ||||
|  | ||||
| // https://developer.mozilla.org/en-US/docs/Web/API/Request/Request | ||||
| const Request = @This(); | ||||
|  | ||||
| method: Http.Method, | ||||
| url: []const u8, | ||||
| body: []const u8, | ||||
|  | ||||
| pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Request { | ||||
|     const arena = page.arena; | ||||
|     const options: RequestInit = _options orelse .{}; | ||||
|  | ||||
|     const url = blk: switch (input) { | ||||
|         .string => |str| { | ||||
| @@ -42,13 +60,146 @@ pub fn constructor(input: RequestInput, page: *Page) !Request { | ||||
|         }, | ||||
|     }; | ||||
|  | ||||
|     const method: Http.Method = blk: for (std.enums.values(Http.Method)) |method| { | ||||
|         if (std.ascii.eqlIgnoreCase(options.method, @tagName(method))) { | ||||
|             break :blk method; | ||||
|         } | ||||
|     } else { | ||||
|         return error.InvalidMethod; | ||||
|     }; | ||||
|  | ||||
|     const body = try arena.dupe(u8, options.body); | ||||
|  | ||||
|     return .{ | ||||
|         .method = method, | ||||
|         .url = url, | ||||
|         .body = body, | ||||
|     }; | ||||
| } | ||||
|  | ||||
| pub fn get_url(self: *const Request, page: *Page) ![]const u8 { | ||||
|     return try page.arena.dupe(u8, self.url); | ||||
| pub fn get_url(self: *const Request) []const u8 { | ||||
|     return self.url; | ||||
| } | ||||
|  | ||||
| pub fn get_method(self: *const Request) []const u8 { | ||||
|     return @tagName(self.method); | ||||
| } | ||||
|  | ||||
| pub fn get_body(self: *const Request) []const u8 { | ||||
|     return self.body; | ||||
| } | ||||
|  | ||||
| const FetchContext = struct { | ||||
|     arena: std.mem.Allocator, | ||||
|     js_ctx: *Env.JsContext, | ||||
|     promise_resolver: v8.Persistent(v8.PromiseResolver), | ||||
|  | ||||
|     body: std.ArrayListUnmanaged(u8) = .empty, | ||||
|     headers: std.ArrayListUnmanaged([]const u8) = .empty, | ||||
|     status: u16 = 0, | ||||
|     mime: ?Mime = null, | ||||
|     transfer: ?*HttpClient.Transfer = null, | ||||
|  | ||||
|     /// This effectively takes ownership of the FetchContext. | ||||
|     /// | ||||
|     /// We just return the underlying slices used for `headers` | ||||
|     /// and for `body` here to avoid an allocation. | ||||
|     pub fn toResponse(self: FetchContext) !Response { | ||||
|         return Response{ | ||||
|             .status = self.status, | ||||
|             .headers = self.headers.items, | ||||
|             .mime = self.mime, | ||||
|             .body = self.body.items, | ||||
|         }; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| // https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch | ||||
| pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !Env.Promise { | ||||
|     const arena = page.arena; | ||||
|  | ||||
|     const req = try Request.constructor(input, options, page); | ||||
|     const resolver = Env.PromiseResolver{ | ||||
|         .js_context = page.main_context, | ||||
|         .resolver = v8.PromiseResolver.init(page.main_context.v8_context), | ||||
|     }; | ||||
|  | ||||
|     const client = page.http_client; | ||||
|     const headers = try HttpClient.Headers.init(); | ||||
|  | ||||
|     const fetch_ctx = try arena.create(FetchContext); | ||||
|     fetch_ctx.* = .{ | ||||
|         .arena = page.arena, | ||||
|         .js_ctx = page.main_context, | ||||
|         .promise_resolver = v8.Persistent(v8.PromiseResolver).init(page.main_context.isolate, resolver.resolver), | ||||
|     }; | ||||
|  | ||||
|     try client.request(.{ | ||||
|         .method = req.method, | ||||
|         .url = try arena.dupeZ(u8, req.url), | ||||
|         .headers = headers, | ||||
|         .body = req.body, | ||||
|         .cookie_jar = page.cookie_jar, | ||||
|         .ctx = @ptrCast(fetch_ctx), | ||||
|  | ||||
|         .start_callback = struct { | ||||
|             fn startCallback(transfer: *HttpClient.Transfer) !void { | ||||
|                 const self: *FetchContext = @alignCast(@ptrCast(transfer.ctx)); | ||||
|                 self.transfer = transfer; | ||||
|             } | ||||
|         }.startCallback, | ||||
|         .header_callback = struct { | ||||
|             fn headerCallback(transfer: *HttpClient.Transfer, header: []const u8) !void { | ||||
|                 const self: *FetchContext = @alignCast(@ptrCast(transfer.ctx)); | ||||
|                 try self.headers.append(self.arena, try self.arena.dupe(u8, header)); | ||||
|             } | ||||
|         }.headerCallback, | ||||
|         .header_done_callback = struct { | ||||
|             fn headerDoneCallback(transfer: *HttpClient.Transfer) !void { | ||||
|                 const self: *FetchContext = @alignCast(@ptrCast(transfer.ctx)); | ||||
|                 const header = &transfer.response_header.?; | ||||
|  | ||||
|                 if (header.contentType()) |ct| { | ||||
|                     self.mime = Mime.parse(ct) catch { | ||||
|                         return error.Todo; | ||||
|                     }; | ||||
|                 } | ||||
|  | ||||
|                 self.status = header.status; | ||||
|             } | ||||
|         }.headerDoneCallback, | ||||
|         .data_callback = struct { | ||||
|             fn dataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void { | ||||
|                 const self: *FetchContext = @alignCast(@ptrCast(transfer.ctx)); | ||||
|                 try self.body.appendSlice(self.arena, data); | ||||
|             } | ||||
|         }.dataCallback, | ||||
|         .done_callback = struct { | ||||
|             fn doneCallback(ctx: *anyopaque) !void { | ||||
|                 const self: *FetchContext = @alignCast(@ptrCast(ctx)); | ||||
|                 const response = try self.toResponse(); | ||||
|                 const promise_resolver: Env.PromiseResolver = .{ | ||||
|                     .js_context = self.js_ctx, | ||||
|                     .resolver = self.promise_resolver.castToPromiseResolver(), | ||||
|                 }; | ||||
|  | ||||
|                 try promise_resolver.resolve(response); | ||||
|             } | ||||
|         }.doneCallback, | ||||
|         .error_callback = struct { | ||||
|             fn errorCallback(ctx: *anyopaque, err: anyerror) void { | ||||
|                 const self: *FetchContext = @alignCast(@ptrCast(ctx)); | ||||
|                 const promise_resolver: Env.PromiseResolver = .{ | ||||
|                     .js_context = self.js_ctx, | ||||
|                     .resolver = self.promise_resolver.castToPromiseResolver(), | ||||
|                 }; | ||||
|  | ||||
|                 promise_resolver.reject(@errorName(err)) catch unreachable; | ||||
|             } | ||||
|         }.errorCallback, | ||||
|     }); | ||||
|  | ||||
|     return resolver.promise(); | ||||
| } | ||||
|  | ||||
| const testing = @import("../../testing.zig"); | ||||
| @@ -59,10 +210,44 @@ test "fetch: request" { | ||||
|     try runner.testCases(&.{ | ||||
|         .{ "let request = new Request('flower.png')", "undefined" }, | ||||
|         .{ "request.url", "https://lightpanda.io/flower.png" }, | ||||
|         .{ "request.method", "GET" }, | ||||
|     }, .{}); | ||||
|  | ||||
|     try runner.testCases(&.{ | ||||
|         .{ "let request2 = new Request('https://google.com')", "undefined" }, | ||||
|         .{ "let request2 = new Request('https://google.com', { method: 'POST', body: 'Hello, World' })", "undefined" }, | ||||
|         .{ "request2.url", "https://google.com" }, | ||||
|         .{ "request2.method", "POST" }, | ||||
|         .{ "request2.body", "Hello, World" }, | ||||
|     }, .{}); | ||||
| } | ||||
|  | ||||
| test "fetch: Browser.fetch" { | ||||
|     var runner = try testing.jsRunner(testing.tracking_allocator, .{}); | ||||
|     defer runner.deinit(); | ||||
|  | ||||
|     try runner.testCases(&.{ | ||||
|         .{ | ||||
|             \\  var ok = false; | ||||
|             \\  const request = new Request("http://127.0.0.1:9582/loader"); | ||||
|             \\  fetch(request).then((response) => { ok = response.ok; }); | ||||
|             \\  false; | ||||
|             , | ||||
|             "false", | ||||
|         }, | ||||
|         // all events have been resolved. | ||||
|         .{ "ok", "true" }, | ||||
|     }, .{}); | ||||
|  | ||||
|     try runner.testCases(&.{ | ||||
|         .{ | ||||
|             \\  var ok2 = false; | ||||
|             \\  const request2 = new Request("http://127.0.0.1:9582/loader"); | ||||
|             \\  (async function () { resp = await fetch(request2); ok2 = resp.ok; }()); | ||||
|             \\  false; | ||||
|             , | ||||
|             "false", | ||||
|         }, | ||||
|         // all events have been resolved. | ||||
|         .{ "ok2", "true" }, | ||||
|     }, .{}); | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,64 @@ | ||||
| // 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 URL = @import("../../url.zig").URL; | ||||
| const Page = @import("../page.zig").Page; | ||||
|  | ||||
| const Http = @import("../../http/Http.zig"); | ||||
| const HttpClient = @import("../../http/Client.zig"); | ||||
| const Mime = @import("../mime.zig").Mime; | ||||
|  | ||||
| // https://developer.mozilla.org/en-US/docs/Web/API/Response | ||||
| const Response = @This(); | ||||
|  | ||||
| status: u16 = 0, | ||||
| headers: []const []const u8, | ||||
| mime: ?Mime = null, | ||||
| body: []const u8, | ||||
|  | ||||
| const ResponseInput = union(enum) { | ||||
|     string: []const u8, | ||||
| }; | ||||
|  | ||||
| pub fn constructor(input: ResponseInput, page: *Page) !Response { | ||||
|     const arena = page.arena; | ||||
|  | ||||
|     const body = blk: switch (input) { | ||||
|         .string => |str| { | ||||
|             break :blk try arena.dupe(u8, str); | ||||
|         }, | ||||
|     }; | ||||
|  | ||||
|     return .{ | ||||
|         .body = body, | ||||
|         .headers = &[_][]const u8{}, | ||||
|     }; | ||||
| } | ||||
|  | ||||
| pub fn get_ok(self: *const Response) bool { | ||||
|     return self.status >= 200 and self.status <= 299; | ||||
| } | ||||
|  | ||||
| const testing = @import("../../testing.zig"); | ||||
| test "fetch: response" { | ||||
|     var runner = try testing.jsRunner(testing.tracking_allocator, .{ .url = "https://lightpanda.io" }); | ||||
|     defer runner.deinit(); | ||||
|  | ||||
|     try runner.testCases(&.{}, .{}); | ||||
| } | ||||
|   | ||||
| @@ -39,6 +39,9 @@ const Css = @import("../css/css.zig").Css; | ||||
| const Function = Env.Function; | ||||
| const JsObject = Env.JsObject; | ||||
|  | ||||
| const v8 = @import("v8"); | ||||
| const Request = @import("../fetch/Request.zig"); | ||||
|  | ||||
| const storage = @import("../storage/storage.zig"); | ||||
|  | ||||
| // https://dom.spec.whatwg.org/#interface-window-extensions | ||||
| @@ -95,6 +98,10 @@ pub const Window = struct { | ||||
|         self.storage_shelf = shelf; | ||||
|     } | ||||
|  | ||||
|     pub fn _fetch(_: *Window, input: Request.RequestInput, options: ?Request.RequestInit, page: *Page) !Env.Promise { | ||||
|         return Request.fetch(input, options, page); | ||||
|     } | ||||
|  | ||||
|     pub fn get_window(self: *Window) *Window { | ||||
|         return self; | ||||
|     } | ||||
|   | ||||
| @@ -339,13 +339,13 @@ pub const Opts = struct { | ||||
|     proxy_bearer_token: ?[:0]const u8 = null, | ||||
| }; | ||||
|  | ||||
| pub const Method = enum { | ||||
|     GET, | ||||
|     PUT, | ||||
|     POST, | ||||
|     DELETE, | ||||
|     HEAD, | ||||
|     OPTIONS, | ||||
| pub const Method = enum(u8) { | ||||
|     GET = 0, | ||||
|     PUT = 1, | ||||
|     POST = 2, | ||||
|     DELETE = 3, | ||||
|     HEAD = 4, | ||||
|     OPTIONS = 5, | ||||
| }; | ||||
|  | ||||
| // TODO: on BSD / Linux, we could just read the PEM file directly. | ||||
|   | ||||
| @@ -2178,6 +2178,17 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { | ||||
|                     return error.FailedToResolvePromise; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             pub fn reject(self: PromiseResolver, value: anytype) !void { | ||||
|                 const js_context = self.js_context; | ||||
|                 const js_value = try js_context.zigValueToJs(value); | ||||
|  | ||||
|                 // resolver.reject will return null if the promise isn't pending | ||||
|                 const ok = self.resolver.reject(js_context.v8_context, js_value) orelse return; | ||||
|                 if (!ok) { | ||||
|                     return error.FailedToRejectPromise; | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         pub const Promise = struct { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Muki Kiboigo
					Muki Kiboigo