From 066df87dd433283ae759f0ed329c6fccede15d89 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Tue, 2 Sep 2025 20:25:48 -0700 Subject: [PATCH] move fetch() into fetch.zig --- src/browser/fetch/fetch.zig | 158 ++++++++++++++++++++++++++++++++++++ src/browser/html/window.zig | 3 +- 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/src/browser/fetch/fetch.zig b/src/browser/fetch/fetch.zig index 9b776074..4f1c3d3b 100644 --- a/src/browser/fetch/fetch.zig +++ b/src/browser/fetch/fetch.zig @@ -16,8 +16,166 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const std = @import("std"); +const log = @import("../../log.zig"); + +const v8 = @import("v8"); +const Env = @import("../env.zig").Env; +const Page = @import("../page.zig").Page; + +const Http = @import("../../http/Http.zig"); +const HttpClient = @import("../../http/Client.zig"); +const Mime = @import("../mime.zig").Mime; + +const RequestInput = @import("Request.zig").RequestInput; +const RequestInit = @import("Request.zig").RequestInit; +const Request = @import("Request.zig"); +const Response = @import("./Response.zig"); + pub const Interfaces = .{ @import("Headers.zig"), @import("Request.zig"), @import("Response.zig"), }; + +const FetchContext = struct { + arena: std.mem.Allocator, + js_ctx: *Env.JsContext, + promise_resolver: v8.Persistent(v8.PromiseResolver), + + method: Http.Method, + url: []const u8, + 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: *const 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), + }; + + var headers = try Http.Headers.init(); + try page.requestCookie(.{}).headersForRequest(arena, req.url, &headers); + + const fetch_ctx = try arena.create(FetchContext); + fetch_ctx.* = .{ + .arena = arena, + .js_ctx = page.main_context, + .promise_resolver = v8.Persistent(v8.PromiseResolver).init( + page.main_context.isolate, + resolver.resolver, + ), + .method = req.method, + .url = req.url, + }; + + try page.http_client.request(.{ + .ctx = @ptrCast(fetch_ctx), + .url = req.url, + .method = req.method, + .headers = headers, + .body = req.body, + .cookie_jar = page.cookie_jar, + .resource_type = .fetch, + + .start_callback = struct { + fn startCallback(transfer: *HttpClient.Transfer) !void { + const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx)); + log.debug(.http, "request start", .{ .method = self.method, .url = self.url, .source = "fetch" }); + + self.transfer = transfer; + } + }.startCallback, + .header_callback = struct { + fn headerCallback(transfer: *HttpClient.Transfer) !void { + const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx)); + + const header = &transfer.response_header.?; + + log.debug(.http, "request header", .{ + .source = "fetch", + .method = self.method, + .url = self.url, + .status = header.status, + }); + + if (header.contentType()) |ct| { + self.mime = Mime.parse(ct) catch { + return error.MimeParsing; + }; + } + + var it = transfer.responseHeaderIterator(); + while (it.next()) |hdr| { + const joined = try std.fmt.allocPrint(self.arena, "{s}: {s}", .{ hdr.name, hdr.value }); + try self.headers.append(self.arena, joined); + } + + self.status = header.status; + } + }.headerCallback, + .data_callback = struct { + fn dataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void { + const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx)); + try self.body.appendSlice(self.arena, data); + } + }.dataCallback, + .done_callback = struct { + fn doneCallback(ctx: *anyopaque) !void { + const self: *FetchContext = @ptrCast(@alignCast(ctx)); + + log.info(.http, "request complete", .{ + .source = "fetch", + .method = self.method, + .url = self.url, + .status = self.status, + }); + + 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 = @ptrCast(@alignCast(ctx)); + + self.transfer = null; + 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(); +} diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index cebfc902..790e823f 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -41,6 +41,7 @@ const JsObject = Env.JsObject; const v8 = @import("v8"); const Request = @import("../fetch/Request.zig"); +const fetchFn = @import("../fetch/fetch.zig").fetch; const storage = @import("../storage/storage.zig"); @@ -99,7 +100,7 @@ pub const Window = struct { } pub fn _fetch(_: *Window, input: Request.RequestInput, options: ?Request.RequestInit, page: *Page) !Env.Promise { - return Request.fetch(input, options, page); + return fetchFn(input, options, page); } pub fn get_window(self: *Window) *Window {