mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 22:53:28 +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