mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 23:23: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/form_data.zig").Interfaces,
|
||||||
@import("xhr/File.zig"),
|
@import("xhr/File.zig"),
|
||||||
@import("xmlserializer/xmlserializer.zig").Interfaces,
|
@import("xmlserializer/xmlserializer.zig").Interfaces,
|
||||||
@import("fetch/Request.zig"),
|
|
||||||
@import("fetch/Headers.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 URL = @import("../../url.zig").URL;
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
|
const Response = @import("./Response.zig");
|
||||||
const Request = @This();
|
|
||||||
|
|
||||||
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,
|
string: []const u8,
|
||||||
request: Request,
|
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 arena = page.arena;
|
||||||
|
const options: RequestInit = _options orelse .{};
|
||||||
|
|
||||||
const url = blk: switch (input) {
|
const url = blk: switch (input) {
|
||||||
.string => |str| {
|
.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 .{
|
return .{
|
||||||
|
.method = method,
|
||||||
.url = url,
|
.url = url,
|
||||||
|
.body = body,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_url(self: *const Request, page: *Page) ![]const u8 {
|
pub fn get_url(self: *const Request) []const u8 {
|
||||||
return try page.arena.dupe(u8, self.url);
|
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");
|
const testing = @import("../../testing.zig");
|
||||||
@@ -59,10 +210,44 @@ test "fetch: request" {
|
|||||||
try runner.testCases(&.{
|
try runner.testCases(&.{
|
||||||
.{ "let request = new Request('flower.png')", "undefined" },
|
.{ "let request = new Request('flower.png')", "undefined" },
|
||||||
.{ "request.url", "https://lightpanda.io/flower.png" },
|
.{ "request.url", "https://lightpanda.io/flower.png" },
|
||||||
|
.{ "request.method", "GET" },
|
||||||
}, .{});
|
}, .{});
|
||||||
|
|
||||||
try runner.testCases(&.{
|
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.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 Function = Env.Function;
|
||||||
const JsObject = Env.JsObject;
|
const JsObject = Env.JsObject;
|
||||||
|
|
||||||
|
const v8 = @import("v8");
|
||||||
|
const Request = @import("../fetch/Request.zig");
|
||||||
|
|
||||||
const storage = @import("../storage/storage.zig");
|
const storage = @import("../storage/storage.zig");
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#interface-window-extensions
|
// https://dom.spec.whatwg.org/#interface-window-extensions
|
||||||
@@ -95,6 +98,10 @@ pub const Window = struct {
|
|||||||
self.storage_shelf = shelf;
|
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 {
|
pub fn get_window(self: *Window) *Window {
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -339,13 +339,13 @@ pub const Opts = struct {
|
|||||||
proxy_bearer_token: ?[:0]const u8 = null,
|
proxy_bearer_token: ?[:0]const u8 = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Method = enum {
|
pub const Method = enum(u8) {
|
||||||
GET,
|
GET = 0,
|
||||||
PUT,
|
PUT = 1,
|
||||||
POST,
|
POST = 2,
|
||||||
DELETE,
|
DELETE = 3,
|
||||||
HEAD,
|
HEAD = 4,
|
||||||
OPTIONS,
|
OPTIONS = 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: on BSD / Linux, we could just read the PEM file directly.
|
// 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;
|
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 {
|
pub const Promise = struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user