Skip to content

Commit 78e7482

Browse files
committed
initial fetch in zig
1 parent 82a6e72 commit 78e7482

File tree

6 files changed

+284
-16
lines changed

6 files changed

+284
-16
lines changed

src/browser/env.zig

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ const WebApis = struct {
3636
@import("xhr/form_data.zig").Interfaces,
3737
@import("xhr/File.zig"),
3838
@import("xmlserializer/xmlserializer.zig").Interfaces,
39-
@import("fetch/Request.zig"),
4039
@import("fetch/Headers.zig"),
40+
@import("fetch/Request.zig"),
41+
@import("fetch/Response.zig"),
4142
});
4243
};
4344

src/browser/fetch/Request.zig

Lines changed: 193 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,36 @@ const std = @import("std");
2020
const URL = @import("../../url.zig").URL;
2121
const Page = @import("../page.zig").Page;
2222

23-
// https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
24-
const Request = @This();
23+
const Response = @import("./Response.zig");
2524

26-
url: []const u8,
25+
const Http = @import("../../http/Http.zig");
26+
const HttpClient = @import("../../http/Client.zig");
27+
const Mime = @import("../mime.zig").Mime;
2728

28-
const RequestInput = union(enum) {
29+
const v8 = @import("v8");
30+
const Env = @import("../env.zig").Env;
31+
32+
pub const RequestInput = union(enum) {
2933
string: []const u8,
3034
request: Request,
3135
};
3236

33-
pub fn constructor(input: RequestInput, page: *Page) !Request {
37+
// https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
38+
pub const RequestInit = struct {
39+
method: []const u8 = "GET",
40+
body: []const u8 = "",
41+
};
42+
43+
// https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
44+
const Request = @This();
45+
46+
method: Http.Method,
47+
url: []const u8,
48+
body: []const u8,
49+
50+
pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Request {
3451
const arena = page.arena;
52+
const options: RequestInit = _options orelse .{};
3553

3654
const url = blk: switch (input) {
3755
.string => |str| {
@@ -42,13 +60,146 @@ pub fn constructor(input: RequestInput, page: *Page) !Request {
4260
},
4361
};
4462

63+
const method: Http.Method = blk: for (std.enums.values(Http.Method)) |method| {
64+
if (std.ascii.eqlIgnoreCase(options.method, @tagName(method))) {
65+
break :blk method;
66+
}
67+
} else {
68+
return error.InvalidMethod;
69+
};
70+
71+
const body = try arena.dupe(u8, options.body);
72+
4573
return .{
74+
.method = method,
4675
.url = url,
76+
.body = body,
4777
};
4878
}
4979

50-
pub fn get_url(self: *const Request, page: *Page) ![]const u8 {
51-
return try page.arena.dupe(u8, self.url);
80+
pub fn get_url(self: *const Request) []const u8 {
81+
return self.url;
82+
}
83+
84+
pub fn get_method(self: *const Request) []const u8 {
85+
return @tagName(self.method);
86+
}
87+
88+
pub fn get_body(self: *const Request) []const u8 {
89+
return self.body;
90+
}
91+
92+
const FetchContext = struct {
93+
arena: std.mem.Allocator,
94+
js_ctx: *Env.JsContext,
95+
promise_resolver: v8.Persistent(v8.PromiseResolver),
96+
97+
body: std.ArrayListUnmanaged(u8) = .empty,
98+
headers: std.ArrayListUnmanaged([]const u8) = .empty,
99+
status: u16 = 0,
100+
mime: ?Mime = null,
101+
transfer: ?*HttpClient.Transfer = null,
102+
103+
/// This effectively takes ownership of the FetchContext.
104+
///
105+
/// We just return the underlying slices used for `headers`
106+
/// and for `body` here to avoid an allocation.
107+
pub fn toResponse(self: FetchContext) !Response {
108+
return Response{
109+
.status = self.status,
110+
.headers = self.headers.items,
111+
.mime = self.mime,
112+
.body = self.body.items,
113+
};
114+
}
115+
};
116+
117+
// https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
118+
pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !Env.Promise {
119+
const arena = page.arena;
120+
121+
const req = try Request.constructor(input, options, page);
122+
const resolver = Env.PromiseResolver{
123+
.js_context = page.main_context,
124+
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
125+
};
126+
127+
const client = page.http_client;
128+
const headers = try HttpClient.Headers.init();
129+
130+
const fetch_ctx = try arena.create(FetchContext);
131+
fetch_ctx.* = .{
132+
.arena = page.arena,
133+
.js_ctx = page.main_context,
134+
.promise_resolver = v8.Persistent(v8.PromiseResolver).init(page.main_context.isolate, resolver.resolver),
135+
};
136+
137+
try client.request(.{
138+
.method = req.method,
139+
.url = try arena.dupeZ(u8, req.url),
140+
.headers = headers,
141+
.body = req.body,
142+
.cookie_jar = page.cookie_jar,
143+
.ctx = @ptrCast(fetch_ctx),
144+
145+
.start_callback = struct {
146+
fn startCallback(transfer: *HttpClient.Transfer) !void {
147+
const self: *FetchContext = @alignCast(@ptrCast(transfer.ctx));
148+
self.transfer = transfer;
149+
}
150+
}.startCallback,
151+
.header_callback = struct {
152+
fn headerCallback(transfer: *HttpClient.Transfer, header: []const u8) !void {
153+
const self: *FetchContext = @alignCast(@ptrCast(transfer.ctx));
154+
try self.headers.append(self.arena, try self.arena.dupe(u8, header));
155+
}
156+
}.headerCallback,
157+
.header_done_callback = struct {
158+
fn headerDoneCallback(transfer: *HttpClient.Transfer) !void {
159+
const self: *FetchContext = @alignCast(@ptrCast(transfer.ctx));
160+
const header = &transfer.response_header.?;
161+
162+
if (header.contentType()) |ct| {
163+
self.mime = Mime.parse(ct) catch {
164+
return error.Todo;
165+
};
166+
}
167+
168+
self.status = header.status;
169+
}
170+
}.headerDoneCallback,
171+
.data_callback = struct {
172+
fn dataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
173+
const self: *FetchContext = @alignCast(@ptrCast(transfer.ctx));
174+
try self.body.appendSlice(self.arena, data);
175+
}
176+
}.dataCallback,
177+
.done_callback = struct {
178+
fn doneCallback(ctx: *anyopaque) !void {
179+
const self: *FetchContext = @alignCast(@ptrCast(ctx));
180+
const response = try self.toResponse();
181+
const promise_resolver: Env.PromiseResolver = .{
182+
.js_context = self.js_ctx,
183+
.resolver = self.promise_resolver.castToPromiseResolver(),
184+
};
185+
186+
try promise_resolver.resolve(response);
187+
}
188+
}.doneCallback,
189+
.error_callback = struct {
190+
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
191+
const self: *FetchContext = @alignCast(@ptrCast(ctx));
192+
const promise_resolver: Env.PromiseResolver = .{
193+
.js_context = self.js_ctx,
194+
.resolver = self.promise_resolver.castToPromiseResolver(),
195+
};
196+
197+
promise_resolver.reject(@errorName(err)) catch unreachable;
198+
}
199+
}.errorCallback,
200+
});
201+
202+
return resolver.promise();
52203
}
53204

54205
const testing = @import("../../testing.zig");
@@ -59,10 +210,44 @@ test "fetch: request" {
59210
try runner.testCases(&.{
60211
.{ "let request = new Request('flower.png')", "undefined" },
61212
.{ "request.url", "https://lightpanda.io/flower.png" },
213+
.{ "request.method", "GET" },
62214
}, .{});
63215

64216
try runner.testCases(&.{
65-
.{ "let request2 = new Request('https://google.com')", "undefined" },
217+
.{ "let request2 = new Request('https://google.com', { method: 'POST', body: 'Hello, World' })", "undefined" },
66218
.{ "request2.url", "https://google.com" },
219+
.{ "request2.method", "POST" },
220+
.{ "request2.body", "Hello, World" },
221+
}, .{});
222+
}
223+
224+
test "fetch: Browser.fetch" {
225+
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
226+
defer runner.deinit();
227+
228+
try runner.testCases(&.{
229+
.{
230+
\\ var ok = false;
231+
\\ const request = new Request("http://127.0.0.1:9582/loader");
232+
\\ fetch(request).then((response) => { ok = response.ok; });
233+
\\ false;
234+
,
235+
"false",
236+
},
237+
// all events have been resolved.
238+
.{ "ok", "true" },
239+
}, .{});
240+
241+
try runner.testCases(&.{
242+
.{
243+
\\ var ok2 = false;
244+
\\ const request2 = new Request("http://127.0.0.1:9582/loader");
245+
\\ (async function () { resp = await fetch(request2); ok2 = resp.ok; }());
246+
\\ false;
247+
,
248+
"false",
249+
},
250+
// all events have been resolved.
251+
.{ "ok2", "true" },
67252
}, .{});
68253
}

src/browser/fetch/Response.zig

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <[email protected]>
4+
// Pierre Tachoire <[email protected]>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
const std = @import("std");
20+
const URL = @import("../../url.zig").URL;
21+
const Page = @import("../page.zig").Page;
22+
23+
const Http = @import("../../http/Http.zig");
24+
const HttpClient = @import("../../http/Client.zig");
25+
const Mime = @import("../mime.zig").Mime;
26+
27+
// https://developer.mozilla.org/en-US/docs/Web/API/Response
28+
const Response = @This();
29+
30+
status: u16 = 0,
31+
headers: []const []const u8,
32+
mime: ?Mime = null,
33+
body: []const u8,
34+
35+
const ResponseInput = union(enum) {
36+
string: []const u8,
37+
};
38+
39+
pub fn constructor(input: ResponseInput, page: *Page) !Response {
40+
const arena = page.arena;
41+
42+
const body = blk: switch (input) {
43+
.string => |str| {
44+
break :blk try arena.dupe(u8, str);
45+
},
46+
};
47+
48+
return .{
49+
.body = body,
50+
.headers = &[_][]const u8{},
51+
};
52+
}
53+
54+
pub fn get_ok(self: *const Response) bool {
55+
return self.status >= 200 and self.status <= 299;
56+
}
57+
58+
const testing = @import("../../testing.zig");
59+
test "fetch: response" {
60+
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .url = "https://lightpanda.io" });
61+
defer runner.deinit();
62+
63+
try runner.testCases(&.{}, .{});
64+
}

src/browser/html/window.zig

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ const Css = @import("../css/css.zig").Css;
3939
const Function = Env.Function;
4040
const JsObject = Env.JsObject;
4141

42+
const v8 = @import("v8");
43+
const Request = @import("../fetch/Request.zig");
44+
4245
const storage = @import("../storage/storage.zig");
4346

4447
// https://dom.spec.whatwg.org/#interface-window-extensions
@@ -95,6 +98,10 @@ pub const Window = struct {
9598
self.storage_shelf = shelf;
9699
}
97100

101+
pub fn _fetch(_: *Window, input: Request.RequestInput, options: ?Request.RequestInit, page: *Page) !Env.Promise {
102+
return Request.fetch(input, options, page);
103+
}
104+
98105
pub fn get_window(self: *Window) *Window {
99106
return self;
100107
}

src/http/Http.zig

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -339,13 +339,13 @@ pub const Opts = struct {
339339
proxy_bearer_token: ?[:0]const u8 = null,
340340
};
341341

342-
pub const Method = enum {
343-
GET,
344-
PUT,
345-
POST,
346-
DELETE,
347-
HEAD,
348-
OPTIONS,
342+
pub const Method = enum(u8) {
343+
GET = 0,
344+
PUT = 1,
345+
POST = 2,
346+
DELETE = 3,
347+
HEAD = 4,
348+
OPTIONS = 5,
349349
};
350350

351351
// TODO: on BSD / Linux, we could just read the PEM file directly.

src/runtime/js.zig

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2178,6 +2178,17 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
21782178
return error.FailedToResolvePromise;
21792179
}
21802180
}
2181+
2182+
pub fn reject(self: PromiseResolver, value: anytype) !void {
2183+
const js_context = self.js_context;
2184+
const js_value = try js_context.zigValueToJs(value);
2185+
2186+
// resolver.reject will return null if the promise isn't pending
2187+
const ok = self.resolver.reject(js_context.v8_context, js_value) orelse return;
2188+
if (!ok) {
2189+
return error.FailedToRejectPromise;
2190+
}
2191+
}
21812192
};
21822193

21832194
pub const Promise = struct {

0 commit comments

Comments
 (0)