@@ -20,18 +20,36 @@ const std = @import("std");
2020const URL = @import ("../../url.zig" ).URL ;
2121const 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
54205const 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}
0 commit comments