43 Commits

Author SHA1 Message Date
Karl Seguin
afa0d5ba12 Try fixing segfault by not being as aggressive with freeing Persisted Resolvers 2025-09-17 20:14:13 +08:00
Muki Kiboigo
4d1e416299 use fetch logging scope, clean some comments 2025-09-16 12:41:35 -07:00
Muki Kiboigo
3badcdbdbd stop using destructor callback for fetch 2025-09-16 12:38:50 -07:00
Muki Kiboigo
fcd82b2c14 htmlRunner for ReadableStream tests, fix ReadableStream enqueue 2025-09-16 12:18:44 -07:00
Muki Kiboigo
d0621510cc use Env.PersistentPromiseResolver 2025-09-16 12:09:54 -07:00
Karl Seguin
2a7a8bc2a6 remove meaningless text from test 2025-09-16 11:08:28 -07:00
Karl Seguin
af916dea1d fix arena, add fetch test 2025-09-16 11:08:27 -07:00
Karl Seguin
31335fc4fb Start working on HTMLSlotElement 2025-09-16 11:08:27 -07:00
Muki Kiboigo
c84634093d use content length to reserve body size 2025-09-16 11:08:27 -07:00
Muki Kiboigo
37d8d2642d copy our Request headers into the HTTP client 2025-09-16 11:08:27 -07:00
Muki Kiboigo
0423a178e9 migrate fetch tests to htmlRunner 2025-09-16 11:08:27 -07:00
Muki Kiboigo
7acf67d668 properly handle closed for ReadableStream 2025-09-16 11:08:27 -07:00
Muki Kiboigo
ef1fece40c deinit persistent promise resolver 2025-09-16 11:08:27 -07:00
Muki Kiboigo
ebb590250f simplify cloning of Req/Resp 2025-09-16 11:08:26 -07:00
Muki Kiboigo
03130a95d8 use call arena for json in Req/Resp 2025-09-16 11:08:26 -07:00
Muki Kiboigo
e133717f7f simplify Headers 2025-09-16 11:08:26 -07:00
Muki Kiboigo
968c695da1 headers iterators should not allocate 2025-09-16 11:08:26 -07:00
Muki Kiboigo
707116a030 use destructor callback for FetchContext 2025-09-16 11:08:26 -07:00
Muki Kiboigo
01966f41ff support object as HeadersInit 2025-09-16 11:08:26 -07:00
Muki Kiboigo
141d17dd55 add logging on fetch error callback 2025-09-16 11:08:26 -07:00
sjorsdonkers
a3c2daf306 retain value, avoid str alloc 2025-09-16 11:08:25 -07:00
sjorsdonkers
dc60fac90d avoid explicit memcpy 2025-09-16 11:08:25 -07:00
sjorsdonkers
a5e2e8ea15 remove length check of fixed size 2025-09-16 11:08:25 -07:00
sjorsdonkers
8295c2abe5 jsValueToZig for fixed sized arrays 2025-09-16 11:08:25 -07:00
Muki Kiboigo
5997be89f6 implement remaining ReadableStream functionality 2025-09-16 11:08:25 -07:00
Muki Kiboigo
1c89cfe5d4 working Header iterators 2025-09-16 11:08:25 -07:00
Muki Kiboigo
b5021bd9fa TypeError when Stream is locked 2025-09-16 11:08:25 -07:00
Muki Kiboigo
4fd365b520 cleaning up various Headers routines 2025-09-16 11:08:25 -07:00
Muki Kiboigo
479cd5ab1a use proper Headers in fetch() 2025-09-16 11:08:24 -07:00
Muki Kiboigo
8285cbcaa9 expand Request/Response interfaces 2025-09-16 11:08:24 -07:00
Muki Kiboigo
545d97b5c0 expand Headers interface 2025-09-16 11:08:24 -07:00
Muki Kiboigo
11016abdd3 remove debug logging in ReadableStream 2025-09-16 11:08:24 -07:00
Muki Kiboigo
066df87dd4 move fetch() into fetch.zig 2025-09-16 11:08:24 -07:00
Muki Kiboigo
91899912d8 add bodyUsed checks on Request and Response 2025-09-16 11:08:24 -07:00
Muki Kiboigo
4ceca6b90b more Headers compatibility 2025-09-16 11:08:24 -07:00
Muki Kiboigo
ec936417c6 add fetch to cdp domain 2025-09-16 11:08:23 -07:00
Muki Kiboigo
4b75b33eb3 add json response method 2025-09-16 11:08:23 -07:00
Muki Kiboigo
1d7e731034 basic readable stream working 2025-09-16 11:08:23 -07:00
Muki Kiboigo
ab60f64452 proper fetch method and body setting 2025-09-16 11:08:23 -07:00
Muki Kiboigo
9757ea7b0f fetch callback logging 2025-09-16 11:08:23 -07:00
Muki Kiboigo
855583874f request url as null terminated 2025-09-16 11:08:23 -07:00
Muki Kiboigo
9efc27c2bb initial fetch in zig 2025-09-16 11:08:23 -07:00
Muki Kiboigo
cab5117d85 remove polyfill and add req/resp 2025-09-16 11:08:19 -07:00
25 changed files with 1857 additions and 796 deletions

View File

@@ -36,6 +36,8 @@ const WebApis = struct {
@import("xhr/form_data.zig").Interfaces,
@import("xhr/File.zig"),
@import("xmlserializer/xmlserializer.zig").Interfaces,
@import("fetch/fetch.zig").Interfaces,
@import("streams/streams.zig").Interfaces,
});
};

View File

@@ -0,0 +1,227 @@
// 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 log = @import("../../log.zig");
const URL = @import("../../url.zig").URL;
const Page = @import("../page.zig").Page;
const iterator = @import("../iterator/iterator.zig");
const v8 = @import("v8");
const Env = @import("../env.zig").Env;
// https://developer.mozilla.org/en-US/docs/Web/API/Headers
const Headers = @This();
// Case-Insensitive String HashMap.
// This allows us to avoid having to allocate lowercase keys all the time.
const HeaderHashMap = std.HashMapUnmanaged([]const u8, []const u8, struct {
pub fn hash(_: @This(), s: []const u8) u64 {
var buf: [64]u8 = undefined;
var hasher = std.hash.Wyhash.init(s.len);
var key = s;
while (key.len >= 64) {
const lower = std.ascii.lowerString(buf[0..], key[0..64]);
hasher.update(lower);
key = key[64..];
}
if (key.len > 0) {
const lower = std.ascii.lowerString(buf[0..key.len], key);
hasher.update(lower);
}
return hasher.final();
}
pub fn eql(_: @This(), a: []const u8, b: []const u8) bool {
return std.ascii.eqlIgnoreCase(a, b);
}
}, 80);
headers: HeaderHashMap = .empty,
// They can either be:
//
// 1. An array of string pairs.
// 2. An object with string keys to string values.
// 3. Another Headers object.
pub const HeadersInit = union(enum) {
// List of Pairs of []const u8
strings: []const [2][]const u8,
// Headers
headers: *Headers,
// Mappings
object: Env.JsObject,
};
pub fn constructor(_init: ?HeadersInit, page: *Page) !Headers {
const arena = page.arena;
var headers: HeaderHashMap = .empty;
if (_init) |init| {
switch (init) {
.strings => |kvs| {
for (kvs) |pair| {
const key = try arena.dupe(u8, pair[0]);
const value = try arena.dupe(u8, pair[1]);
try headers.put(arena, key, value);
}
},
.headers => |hdrs| {
var iter = hdrs.headers.iterator();
while (iter.next()) |entry| {
try headers.put(arena, entry.key_ptr.*, entry.value_ptr.*);
}
},
.object => |obj| {
var iter = obj.nameIterator();
while (try iter.next()) |name_value| {
const name = try name_value.toString(arena);
const value = try obj.get(name);
const value_string = try value.toString(arena);
try headers.put(arena, name, value_string);
}
},
}
}
return .{
.headers = headers,
};
}
pub fn append(self: *Headers, name: []const u8, value: []const u8, allocator: std.mem.Allocator) !void {
const key = try allocator.dupe(u8, name);
const gop = try self.headers.getOrPut(allocator, key);
if (gop.found_existing) {
// If we found it, append the value.
const new_value = try std.fmt.allocPrint(allocator, "{s}, {s}", .{ gop.value_ptr.*, value });
gop.value_ptr.* = new_value;
} else {
// Otherwise, we should just put it in.
gop.value_ptr.* = try allocator.dupe(u8, value);
}
}
pub fn _append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
const arena = page.arena;
try self.append(name, value, arena);
}
pub fn _delete(self: *Headers, name: []const u8) void {
_ = self.headers.remove(name);
}
pub const HeadersEntryIterator = struct {
slot: [2][]const u8,
iter: HeaderHashMap.Iterator,
// TODO: these SHOULD be in lexigraphical order but I'm not sure how actually
// important that is.
pub fn _next(self: *HeadersEntryIterator) ?[2][]const u8 {
if (self.iter.next()) |entry| {
self.slot[0] = entry.key_ptr.*;
self.slot[1] = entry.value_ptr.*;
return self.slot;
} else {
return null;
}
}
};
pub fn _entries(self: *const Headers) HeadersEntryIterable {
return .{
.inner = .{
.slot = undefined,
.iter = self.headers.iterator(),
},
};
}
pub fn _forEach(self: *Headers, callback_fn: Env.Function, this_arg: ?Env.JsObject) !void {
var iter = self.headers.iterator();
const cb = if (this_arg) |this| try callback_fn.withThis(this) else callback_fn;
while (iter.next()) |entry| {
try cb.call(void, .{ entry.key_ptr.*, entry.value_ptr.*, self });
}
}
pub fn _get(self: *const Headers, name: []const u8) ?[]const u8 {
return self.headers.get(name);
}
pub fn _has(self: *const Headers, name: []const u8) bool {
return self.headers.contains(name);
}
pub const HeadersKeyIterator = struct {
iter: HeaderHashMap.KeyIterator,
pub fn _next(self: *HeadersKeyIterator) ?[]const u8 {
if (self.iter.next()) |key| {
return key.*;
} else {
return null;
}
}
};
pub fn _keys(self: *const Headers) HeadersKeyIterable {
return .{ .inner = .{ .iter = self.headers.keyIterator() } };
}
pub fn _set(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
const arena = page.arena;
const key = try arena.dupe(u8, name);
const gop = try self.headers.getOrPut(arena, key);
gop.value_ptr.* = try arena.dupe(u8, value);
}
pub const HeadersValueIterator = struct {
iter: HeaderHashMap.ValueIterator,
pub fn _next(self: *HeadersValueIterator) ?[]const u8 {
if (self.iter.next()) |value| {
return value.*;
} else {
return null;
}
}
};
pub fn _values(self: *const Headers) HeadersValueIterable {
return .{ .inner = .{ .iter = self.headers.valueIterator() } };
}
pub const HeadersKeyIterable = iterator.Iterable(HeadersKeyIterator, "HeadersKeyIterator");
pub const HeadersValueIterable = iterator.Iterable(HeadersValueIterator, "HeadersValueIterator");
pub const HeadersEntryIterable = iterator.Iterable(HeadersEntryIterator, "HeadersEntryIterator");
const testing = @import("../../testing.zig");
test "fetch: Headers" {
try testing.htmlRunner("fetch/headers.html");
}

View File

@@ -0,0 +1,266 @@
// 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 log = @import("../../log.zig");
const URL = @import("../../url.zig").URL;
const Page = @import("../page.zig").Page;
const Response = @import("./Response.zig");
const Http = @import("../../http/Http.zig");
const ReadableStream = @import("../streams/ReadableStream.zig");
const v8 = @import("v8");
const Env = @import("../env.zig").Env;
const Headers = @import("Headers.zig");
const HeadersInit = @import("Headers.zig").HeadersInit;
pub const RequestInput = union(enum) {
string: []const u8,
request: *Request,
};
pub const RequestCache = enum {
default,
@"no-store",
reload,
@"no-cache",
@"force-cache",
@"only-if-cached",
pub fn fromString(str: []const u8) ?RequestCache {
for (std.enums.values(RequestCache)) |cache| {
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
return cache;
}
} else {
return null;
}
}
pub fn toString(self: RequestCache) []const u8 {
return @tagName(self);
}
};
pub const RequestCredentials = enum {
omit,
@"same-origin",
include,
pub fn fromString(str: []const u8) ?RequestCredentials {
for (std.enums.values(RequestCredentials)) |cache| {
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
return cache;
}
} else {
return null;
}
}
pub fn toString(self: RequestCredentials) []const u8 {
return @tagName(self);
}
};
// https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
pub const RequestInit = struct {
body: ?[]const u8 = null,
cache: ?[]const u8 = null,
credentials: ?[]const u8 = null,
headers: ?HeadersInit = null,
integrity: ?[]const u8 = null,
method: ?[]const u8 = null,
};
// https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
const Request = @This();
method: Http.Method,
url: [:0]const u8,
cache: RequestCache,
credentials: RequestCredentials,
headers: Headers,
body: ?[]const u8,
body_used: bool = false,
integrity: []const u8,
pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Request {
const arena = page.arena;
const options: RequestInit = _options orelse .{};
const url: [:0]const u8 = blk: switch (input) {
.string => |str| {
break :blk try URL.stitch(arena, str, page.url.raw, .{ .null_terminated = true });
},
.request => |req| {
break :blk try arena.dupeZ(u8, req.url);
},
};
const body = if (options.body) |body| try arena.dupe(u8, body) else null;
const cache = (if (options.cache) |cache| RequestCache.fromString(cache) else null) orelse RequestCache.default;
const credentials = (if (options.credentials) |creds| RequestCredentials.fromString(creds) else null) orelse RequestCredentials.@"same-origin";
const integrity = if (options.integrity) |integ| try arena.dupe(u8, integ) else "";
const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{};
const method: Http.Method = blk: {
if (options.method) |given_method| {
for (std.enums.values(Http.Method)) |method| {
if (std.ascii.eqlIgnoreCase(given_method, @tagName(method))) {
break :blk method;
}
} else {
return error.TypeError;
}
} else {
break :blk Http.Method.GET;
}
};
return .{
.method = method,
.url = url,
.cache = cache,
.credentials = credentials,
.headers = headers,
.body = body,
.integrity = integrity,
};
}
pub fn get_body(self: *const Request, page: *Page) !?*ReadableStream {
if (self.body) |body| {
const stream = try ReadableStream.constructor(null, null, page);
try stream.queue.append(page.arena, body);
return stream;
} else return null;
}
pub fn get_bodyUsed(self: *const Request) bool {
return self.body_used;
}
pub fn get_cache(self: *const Request) RequestCache {
return self.cache;
}
pub fn get_credentials(self: *const Request) RequestCredentials {
return self.credentials;
}
pub fn get_headers(self: *Request) *Headers {
return &self.headers;
}
pub fn get_integrity(self: *const Request) []const u8 {
return self.integrity;
}
// TODO: If we ever support the Navigation API, we need isHistoryNavigation
// https://developer.mozilla.org/en-US/docs/Web/API/Request/isHistoryNavigation
pub fn get_method(self: *const Request) []const u8 {
return @tagName(self.method);
}
pub fn get_url(self: *const Request) []const u8 {
return self.url;
}
pub fn _clone(self: *Request) !Request {
// Not allowed to clone if the body was used.
if (self.body_used) {
return error.TypeError;
}
// OK to just return the same fields BECAUSE
// all of these fields are read-only and can't be modified.
return Request{
.body = self.body,
.body_used = self.body_used,
.cache = self.cache,
.credentials = self.credentials,
.headers = self.headers,
.method = self.method,
.integrity = self.integrity,
.url = self.url,
};
}
pub fn _bytes(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) {
return error.TypeError;
}
const resolver = Env.PromiseResolver{
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
try resolver.resolve(self.body);
self.body_used = true;
return resolver.promise();
}
pub fn _json(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) {
return error.TypeError;
}
const resolver = Env.PromiseResolver{
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
const p = std.json.parseFromSliceLeaky(
std.json.Value,
page.call_arena,
self.body,
.{},
) catch |e| {
log.info(.browser, "invalid json", .{ .err = e, .source = "Request" });
return error.SyntaxError;
};
try resolver.resolve(p);
self.body_used = true;
return resolver.promise();
}
pub fn _text(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) {
return error.TypeError;
}
const resolver = Env.PromiseResolver{
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
try resolver.resolve(self.body);
self.body_used = true;
return resolver.promise();
}
const testing = @import("../../testing.zig");
test "fetch: Request" {
try testing.htmlRunner("fetch/request.html");
}

View File

@@ -0,0 +1,196 @@
// 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 log = @import("../../log.zig");
const v8 = @import("v8");
const HttpClient = @import("../../http/Client.zig");
const Http = @import("../../http/Http.zig");
const URL = @import("../../url.zig").URL;
const ReadableStream = @import("../streams/ReadableStream.zig");
const Headers = @import("Headers.zig");
const HeadersInit = @import("Headers.zig").HeadersInit;
const Env = @import("../env.zig").Env;
const Mime = @import("../mime.zig").Mime;
const Page = @import("../page.zig").Page;
// https://developer.mozilla.org/en-US/docs/Web/API/Response
const Response = @This();
status: u16 = 200,
status_text: []const u8 = "",
headers: Headers,
mime: ?Mime = null,
url: []const u8 = "",
body: []const u8 = "",
body_used: bool = false,
redirected: bool = false,
const ResponseBody = union(enum) {
string: []const u8,
};
const ResponseOptions = struct {
status: u16 = 200,
statusText: ?[]const u8 = null,
headers: ?HeadersInit = null,
};
pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Page) !Response {
const arena = page.arena;
const options: ResponseOptions = _options orelse .{};
const body = blk: {
if (_input) |input| {
switch (input) {
.string => |str| {
break :blk try arena.dupe(u8, str);
},
}
} else {
break :blk "";
}
};
const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{};
const status_text = if (options.statusText) |st| try arena.dupe(u8, st) else "";
return .{
.body = body,
.headers = headers,
.status = options.status,
.status_text = status_text,
};
}
pub fn get_body(self: *const Response, page: *Page) !*ReadableStream {
const stream = try ReadableStream.constructor(null, null, page);
try stream.queue.append(page.arena, self.body);
return stream;
}
pub fn get_bodyUsed(self: *const Response) bool {
return self.body_used;
}
pub fn get_headers(self: *Response) *Headers {
return &self.headers;
}
pub fn get_ok(self: *const Response) bool {
return self.status >= 200 and self.status <= 299;
}
pub fn get_redirected(self: *const Response) bool {
return self.redirected;
}
pub fn get_status(self: *const Response) u16 {
return self.status;
}
pub fn get_statusText(self: *const Response) []const u8 {
return self.status_text;
}
pub fn get_url(self: *const Response) []const u8 {
return self.url;
}
pub fn _clone(self: *const Response) !Response {
if (self.body_used) {
return error.TypeError;
}
// OK to just return the same fields BECAUSE
// all of these fields are read-only and can't be modified.
return Response{
.body = self.body,
.body_used = self.body_used,
.mime = self.mime,
.headers = self.headers,
.redirected = self.redirected,
.status = self.status,
.url = self.url,
};
}
pub fn _bytes(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) {
return error.TypeError;
}
const resolver = Env.PromiseResolver{
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
try resolver.resolve(self.body);
self.body_used = true;
return resolver.promise();
}
pub fn _json(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) {
return error.TypeError;
}
const resolver = Env.PromiseResolver{
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
const p = std.json.parseFromSliceLeaky(
std.json.Value,
page.call_arena,
self.body,
.{},
) catch |e| {
log.info(.browser, "invalid json", .{ .err = e, .source = "Response" });
return error.SyntaxError;
};
try resolver.resolve(p);
self.body_used = true;
return resolver.promise();
}
pub fn _text(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) {
return error.TypeError;
}
const resolver = Env.PromiseResolver{
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
try resolver.resolve(self.body);
self.body_used = true;
return resolver.promise();
}
const testing = @import("../../testing.zig");
test "fetch: Response" {
try testing.htmlRunner("fetch/response.html");
}

212
src/browser/fetch/fetch.zig Normal file
View File

@@ -0,0 +1,212 @@
// 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 log = @import("../../log.zig");
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 Headers = @import("Headers.zig");
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("Headers.zig").HeadersEntryIterable,
@import("Headers.zig").HeadersKeyIterable,
@import("Headers.zig").HeadersValueIterable,
@import("Request.zig"),
@import("Response.zig"),
};
pub const FetchContext = struct {
arena: std.mem.Allocator,
js_ctx: *Env.JsContext,
promise_resolver: Env.PersistentPromiseResolver,
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 {
var headers: Headers = .{};
// convert into Headers
for (self.headers.items) |hdr| {
var iter = std.mem.splitScalar(u8, hdr, ':');
const name = iter.next() orelse "";
const value = iter.next() orelse "";
try headers.append(name, value, self.arena);
}
return Response{
.status = self.status,
.headers = headers,
.mime = self.mime,
.body = self.body.items,
.url = self.url,
};
}
};
// 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);
var headers = try Http.Headers.init();
// Copy our headers into the HTTP headers.
var header_iter = req.headers.headers.iterator();
while (header_iter.next()) |entry| {
const combined = try std.fmt.allocPrintSentinel(
page.arena,
"{s}: {s}",
.{ entry.key_ptr.*, entry.value_ptr.* },
0,
);
try headers.add(combined.ptr);
}
try page.requestCookie(.{}).headersForRequest(arena, req.url, &headers);
const resolver = try page.main_context.createPersistentPromiseResolver();
const fetch_ctx = try arena.create(FetchContext);
fetch_ctx.* = .{
.arena = arena,
.js_ctx = page.main_context,
.promise_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(.fetch, "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(.fetch, "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;
};
}
if (transfer.getContentLength()) |cl| {
try self.body.ensureTotalCapacity(self.arena, cl);
}
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));
self.transfer = null;
log.info(.fetch, "request complete", .{
.source = "fetch",
.method = self.method,
.url = self.url,
.status = self.status,
});
const response = try self.toResponse();
try self.promise_resolver.resolve(response);
}
}.doneCallback,
.error_callback = struct {
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
const self: *FetchContext = @ptrCast(@alignCast(ctx));
self.transfer = null;
log.err(.fetch, "error", .{
.url = self.url,
.err = err,
.source = "fetch error",
});
// We throw an Abort error when the page is getting closed so,
// in this case, we don't need to reject the promise.
if (err != error.Abort) {
self.promise_resolver.reject(@errorName(err)) catch unreachable;
}
}
}.errorCallback,
});
return resolver.promise();
}
const testing = @import("../../testing.zig");
test "fetch: fetch" {
try testing.htmlRunner("fetch/fetch.html");
}

View File

@@ -1042,84 +1042,68 @@ pub const HTMLSlotElement = struct {
flatten: bool = false,
};
pub fn _assignedNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion {
return findAssignedSlotNodes(self, opts_, false, page);
}
// This should return Union, instead of NodeUnion, but we want to re-use
// findAssignedSlotNodes. Returning NodeUnion is fine, as long as every element
// within is an Element. This could be more efficient
pub fn _assignedElements(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion {
return findAssignedSlotNodes(self, opts_, true, page);
}
fn findAssignedSlotNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, element_only: bool, page: *Page) ![]NodeUnion {
const opts = opts_ orelse AssignedNodesOpts{ .flatten = false };
if (opts.flatten) {
log.debug(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" });
if (try findAssignedSlotNodes(self, opts, page)) |nodes| {
return nodes;
}
if (!opts.flatten) {
return &.{};
}
const node: *parser.Node = @ptrCast(@alignCast(self));
const nl = try parser.nodeGetChildNodes(node);
const len = try parser.nodeListLength(nl);
if (len == 0) {
return &.{};
}
// First we look for any explicitly assigned nodes (via the slot attribute)
{
const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name");
var root = try parser.nodeGetRootNode(node);
if (page.getNodeState(root)) |state| {
if (state.shadow_root) |sr| {
root = @ptrCast(@alignCast(sr.host));
}
var assigned = try page.call_arena.alloc(NodeUnion, len);
var i: usize = 0;
while (true) : (i += 1) {
const child = try parser.nodeListItem(nl, @intCast(i)) orelse break;
assigned[i] = try Node.toInterface(child);
}
return assigned[0..i];
}
fn findAssignedSlotNodes(self: *parser.Slot, opts: AssignedNodesOpts, page: *Page) !?[]NodeUnion {
if (opts.flatten) {
log.warn(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" });
}
const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name");
const node: *parser.Node = @ptrCast(@alignCast(self));
var root = try parser.nodeGetRootNode(node);
if (page.getNodeState(root)) |state| {
if (state.shadow_root) |sr| {
root = @ptrCast(@alignCast(sr.host));
}
}
var arr: std.ArrayList(NodeUnion) = .empty;
const w = @import("../dom/walker.zig").WalkerChildren{};
var next: ?*parser.Node = null;
while (true) {
next = try w.get_next(root, next) orelse break;
if (try parser.nodeType(next.?) != .element) {
if (slot_name == null and !element_only) {
// default slot (with no name), takes everything
try arr.append(page.call_arena, try Node.toInterface(next.?));
}
continue;
}
const el: *parser.Element = @ptrCast(@alignCast(next.?));
const element_slot = try parser.elementGetAttribute(el, "slot");
if (nullableStringsAreEqual(slot_name, element_slot)) {
// either they're the same string or they are both null
var arr: std.ArrayList(NodeUnion) = .empty;
const w = @import("../dom/walker.zig").WalkerChildren{};
var next: ?*parser.Node = null;
while (true) {
next = try w.get_next(root, next) orelse break;
if (try parser.nodeType(next.?) != .element) {
if (slot_name == null) {
// default slot (with no name), takes everything
try arr.append(page.call_arena, try Node.toInterface(next.?));
continue;
}
continue;
}
if (arr.items.len > 0) {
return arr.items;
}
const el: *parser.Element = @ptrCast(@alignCast(next.?));
const element_slot = try parser.elementGetAttribute(el, "slot");
if (!opts.flatten) {
return &.{};
if (nullableStringsAreEqual(slot_name, element_slot)) {
// either they're the same string or they are both null
try arr.append(page.call_arena, try Node.toInterface(next.?));
continue;
}
}
// Since, we have no explicitly assigned nodes and flatten == false,
// we'll collect the children of the slot - the defaults.
{
const nl = try parser.nodeGetChildNodes(node);
const len = try parser.nodeListLength(nl);
if (len == 0) {
return &.{};
}
var assigned = try page.call_arena.alloc(NodeUnion, len);
var i: usize = 0;
while (true) : (i += 1) {
const child = try parser.nodeListItem(nl, @intCast(i)) orelse break;
if (!element_only or try parser.nodeType(child) == .element) {
assigned[i] = try Node.toInterface(child);
}
}
return assigned[0..i];
}
return if (arr.items.len == 0) null else arr.items;
}
fn nullableStringsAreEqual(a: ?[]const u8, b: ?[]const u8) bool {
@@ -1345,6 +1329,39 @@ test "Browser: HTML.HtmlScriptElement" {
try testing.htmlRunner("html/script/inline_defer.html");
}
test "Browser: HTML.HtmlSlotElement" {
try testing.htmlRunner("html/slot.html");
test "Browser: HTML.HTMLSlotElement" {
try testing.htmlRunner("html/html_slot_element.html");
}
const Check = struct {
input: []const u8,
expected: ?[]const u8 = null, // Needed when input != expected
};
const bool_valids = [_]Check{
.{ .input = "true" },
.{ .input = "''", .expected = "false" },
.{ .input = "13.5", .expected = "true" },
};
const str_valids = [_]Check{
.{ .input = "'foo'", .expected = "foo" },
.{ .input = "5", .expected = "5" },
.{ .input = "''", .expected = "" },
.{ .input = "document", .expected = "[object HTMLDocument]" },
};
// .{ "elem.type = '5'", "5" },
// .{ "elem.type", "text" },
fn testProperty(
arena: std.mem.Allocator,
runner: *testing.JsRunner,
elem_dot_prop: []const u8,
always: ?[]const u8, // Ignores checks' expected if set
checks: []const Check,
) !void {
for (checks) |check| {
try runner.testCases(&.{
.{ try std.mem.concat(arena, u8, &.{ elem_dot_prop, " = ", check.input }), null },
.{ elem_dot_prop, always orelse check.expected orelse check.input },
}, .{});
}
}

View File

@@ -39,6 +39,10 @@ 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 fetchFn = @import("../fetch/fetch.zig").fetch;
const storage = @import("../storage/storage.zig");
// https://dom.spec.whatwg.org/#interface-window-extensions
@@ -95,6 +99,10 @@ pub const Window = struct {
self.storage_shelf = shelf;
}
pub fn _fetch(_: *Window, input: Request.RequestInput, options: ?Request.RequestInit, page: *Page) !Env.Promise {
return fetchFn(input, options, page);
}
pub fn get_window(self: *Window) *Window {
return self;
}

View File

@@ -1,671 +0,0 @@
// fetch.js code comes from
// https://github.com/JakeChampion/fetch/blob/main/fetch.js
//
// The original code source is available in MIT license.
//
// The script comes from the built version from npm.
// You can get the package with the command:
//
// wget $(npm view whatwg-fetch dist.tarball)
//
// The source is the content of `package/dist/fetch.umd.js` file.
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(factory((global.WHATWGFetch = {})));
}(this, (function (exports) { 'use strict';
/* eslint-disable no-prototype-builtins */
var g =
(typeof globalThis !== 'undefined' && globalThis) ||
(typeof self !== 'undefined' && self) ||
// eslint-disable-next-line no-undef
(typeof global !== 'undefined' && global) ||
{};
var support = {
searchParams: 'URLSearchParams' in g,
iterable: 'Symbol' in g && 'iterator' in Symbol,
blob:
'FileReader' in g &&
'Blob' in g &&
(function() {
try {
new Blob();
return true
} catch (e) {
return false
}
})(),
formData: 'FormData' in g,
// Arraybuffer is available but xhr doesn't implement it for now.
// arrayBuffer: 'ArrayBuffer' in g
arrayBuffer: false
};
function isDataView(obj) {
return obj && DataView.prototype.isPrototypeOf(obj)
}
if (support.arrayBuffer) {
var viewClasses = [
'[object Int8Array]',
'[object Uint8Array]',
'[object Uint8ClampedArray]',
'[object Int16Array]',
'[object Uint16Array]',
'[object Int32Array]',
'[object Uint32Array]',
'[object Float32Array]',
'[object Float64Array]'
];
var isArrayBufferView =
ArrayBuffer.isView ||
function(obj) {
return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
};
}
function normalizeName(name) {
if (typeof name !== 'string') {
name = String(name);
}
if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') {
throw new TypeError('Invalid character in header field name: "' + name + '"')
}
return name.toLowerCase()
}
function normalizeValue(value) {
if (typeof value !== 'string') {
value = String(value);
}
return value
}
// Build a destructive iterator for the value list
function iteratorFor(items) {
var iterator = {
next: function() {
var value = items.shift();
return {done: value === undefined, value: value}
}
};
if (support.iterable) {
iterator[Symbol.iterator] = function() {
return iterator
};
}
return iterator
}
function Headers(headers) {
this.map = {};
if (headers instanceof Headers) {
headers.forEach(function(value, name) {
this.append(name, value);
}, this);
} else if (Array.isArray(headers)) {
headers.forEach(function(header) {
if (header.length != 2) {
throw new TypeError('Headers constructor: expected name/value pair to be length 2, found' + header.length)
}
this.append(header[0], header[1]);
}, this);
} else if (headers) {
Object.getOwnPropertyNames(headers).forEach(function(name) {
this.append(name, headers[name]);
}, this);
}
}
Headers.prototype.append = function(name, value) {
name = normalizeName(name);
value = normalizeValue(value);
var oldValue = this.map[name];
this.map[name] = oldValue ? oldValue + ', ' + value : value;
};
Headers.prototype['delete'] = function(name) {
delete this.map[normalizeName(name)];
};
Headers.prototype.get = function(name) {
name = normalizeName(name);
return this.has(name) ? this.map[name] : null
};
Headers.prototype.has = function(name) {
return this.map.hasOwnProperty(normalizeName(name))
};
Headers.prototype.set = function(name, value) {
this.map[normalizeName(name)] = normalizeValue(value);
};
Headers.prototype.forEach = function(callback, thisArg) {
for (var name in this.map) {
if (this.map.hasOwnProperty(name)) {
callback.call(thisArg, this.map[name], name, this);
}
}
};
Headers.prototype.keys = function() {
var items = [];
this.forEach(function(value, name) {
items.push(name);
});
return iteratorFor(items)
};
Headers.prototype.values = function() {
var items = [];
this.forEach(function(value) {
items.push(value);
});
return iteratorFor(items)
};
Headers.prototype.entries = function() {
var items = [];
this.forEach(function(value, name) {
items.push([name, value]);
});
return iteratorFor(items)
};
if (support.iterable) {
Headers.prototype[Symbol.iterator] = Headers.prototype.entries;
}
function consumed(body) {
if (body._noBody) return
if (body.bodyUsed) {
return Promise.reject(new TypeError('Already read'))
}
body.bodyUsed = true;
}
function fileReaderReady(reader) {
return new Promise(function(resolve, reject) {
reader.onload = function() {
resolve(reader.result);
};
reader.onerror = function() {
reject(reader.error);
};
})
}
function readBlobAsArrayBuffer(blob) {
var reader = new FileReader();
var promise = fileReaderReady(reader);
reader.readAsArrayBuffer(blob);
return promise
}
function readBlobAsText(blob) {
var reader = new FileReader();
var promise = fileReaderReady(reader);
var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type);
var encoding = match ? match[1] : 'utf-8';
reader.readAsText(blob, encoding);
return promise
}
function readArrayBufferAsText(buf) {
var view = new Uint8Array(buf);
var chars = new Array(view.length);
for (var i = 0; i < view.length; i++) {
chars[i] = String.fromCharCode(view[i]);
}
return chars.join('')
}
function bufferClone(buf) {
if (buf.slice) {
return buf.slice(0)
} else {
var view = new Uint8Array(buf.byteLength);
view.set(new Uint8Array(buf));
return view.buffer
}
}
function Body() {
this.bodyUsed = false;
this._initBody = function(body) {
/*
fetch-mock wraps the Response object in an ES6 Proxy to
provide useful test harness features such as flush. However, on
ES5 browsers without fetch or Proxy support pollyfills must be used;
the proxy-pollyfill is unable to proxy an attribute unless it exists
on the object before the Proxy is created. This change ensures
Response.bodyUsed exists on the instance, while maintaining the
semantic of setting Request.bodyUsed in the constructor before
_initBody is called.
*/
// eslint-disable-next-line no-self-assign
this.bodyUsed = this.bodyUsed;
this._bodyInit = body;
if (!body) {
this._noBody = true;
this._bodyText = '';
} else if (typeof body === 'string') {
this._bodyText = body;
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
this._bodyBlob = body;
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
this._bodyFormData = body;
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
this._bodyText = body.toString();
} else if (support.arrayBuffer && support.blob && isDataView(body)) {
this._bodyArrayBuffer = bufferClone(body.buffer);
// IE 10-11 can't handle a DataView body.
this._bodyInit = new Blob([this._bodyArrayBuffer]);
} else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
this._bodyArrayBuffer = bufferClone(body);
} else {
this._bodyText = body = Object.prototype.toString.call(body);
}
if (!this.headers.get('content-type')) {
if (typeof body === 'string') {
this.headers.set('content-type', 'text/plain;charset=UTF-8');
} else if (this._bodyBlob && this._bodyBlob.type) {
this.headers.set('content-type', this._bodyBlob.type);
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
}
}
};
if (support.blob) {
this.blob = function() {
var rejected = consumed(this);
if (rejected) {
return rejected
}
if (this._bodyBlob) {
return Promise.resolve(this._bodyBlob)
} else if (this._bodyArrayBuffer) {
return Promise.resolve(new Blob([this._bodyArrayBuffer]))
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as blob')
} else {
return Promise.resolve(new Blob([this._bodyText]))
}
};
}
this.arrayBuffer = function() {
if (this._bodyArrayBuffer) {
var isConsumed = consumed(this);
if (isConsumed) {
return isConsumed
} else if (ArrayBuffer.isView(this._bodyArrayBuffer)) {
return Promise.resolve(
this._bodyArrayBuffer.buffer.slice(
this._bodyArrayBuffer.byteOffset,
this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength
)
)
} else {
return Promise.resolve(this._bodyArrayBuffer)
}
} else if (support.blob) {
return this.blob().then(readBlobAsArrayBuffer)
} else {
throw new Error('could not read as ArrayBuffer')
}
};
this.text = function() {
var rejected = consumed(this);
if (rejected) {
return rejected
}
if (this._bodyBlob) {
return readBlobAsText(this._bodyBlob)
} else if (this._bodyArrayBuffer) {
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer))
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as text')
} else {
return Promise.resolve(this._bodyText)
}
};
if (support.formData) {
this.formData = function() {
return this.text().then(decode)
};
}
this.json = function() {
return this.text().then(JSON.parse)
};
return this
}
// HTTP methods whose capitalization should be normalized
var methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE'];
function normalizeMethod(method) {
var upcased = method.toUpperCase();
return methods.indexOf(upcased) > -1 ? upcased : method
}
function Request(input, options) {
if (!(this instanceof Request)) {
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
}
options = options || {};
var body = options.body;
if (input instanceof Request) {
if (input.bodyUsed) {
throw new TypeError('Already read')
}
this.url = input.url;
this.credentials = input.credentials;
if (!options.headers) {
this.headers = new Headers(input.headers);
}
this.method = input.method;
this.mode = input.mode;
this.signal = input.signal;
if (!body && input._bodyInit != null) {
body = input._bodyInit;
input.bodyUsed = true;
}
} else {
this.url = String(input);
}
this.credentials = options.credentials || this.credentials || 'same-origin';
if (options.headers || !this.headers) {
this.headers = new Headers(options.headers);
}
this.method = normalizeMethod(options.method || this.method || 'GET');
this.mode = options.mode || this.mode || null;
this.signal = options.signal || this.signal || (function () {
if ('AbortController' in g) {
var ctrl = new AbortController();
return ctrl.signal;
}
}());
this.referrer = null;
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
throw new TypeError('Body not allowed for GET or HEAD requests')
}
this._initBody(body);
if (this.method === 'GET' || this.method === 'HEAD') {
if (options.cache === 'no-store' || options.cache === 'no-cache') {
// Search for a '_' parameter in the query string
var reParamSearch = /([?&])_=[^&]*/;
if (reParamSearch.test(this.url)) {
// If it already exists then set the value with the current time
this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime());
} else {
// Otherwise add a new '_' parameter to the end with the current time
var reQueryString = /\?/;
this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime();
}
}
}
}
Request.prototype.clone = function() {
return new Request(this, {body: this._bodyInit})
};
function decode(body) {
var form = new FormData();
body
.trim()
.split('&')
.forEach(function(bytes) {
if (bytes) {
var split = bytes.split('=');
var name = split.shift().replace(/\+/g, ' ');
var value = split.join('=').replace(/\+/g, ' ');
form.append(decodeURIComponent(name), decodeURIComponent(value));
}
});
return form
}
function parseHeaders(rawHeaders) {
var headers = new Headers();
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
// https://tools.ietf.org/html/rfc7230#section-3.2
var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ');
// Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill
// https://github.com/github/fetch/issues/748
// https://github.com/zloirock/core-js/issues/751
preProcessedHeaders
.split('\r')
.map(function(header) {
return header.indexOf('\n') === 0 ? header.substr(1, header.length) : header
})
.forEach(function(line) {
var parts = line.split(':');
var key = parts.shift().trim();
if (key) {
var value = parts.join(':').trim();
try {
headers.append(key, value);
} catch (error) {
console.warn('Response ' + error.message);
}
}
});
return headers
}
Body.call(Request.prototype);
function Response(bodyInit, options) {
if (!(this instanceof Response)) {
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
}
if (!options) {
options = {};
}
this.type = 'default';
this.status = options.status === undefined ? 200 : options.status;
if (this.status < 200 || this.status > 599) {
throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].")
}
this.ok = this.status >= 200 && this.status < 300;
this.statusText = options.statusText === undefined ? '' : '' + options.statusText;
this.headers = new Headers(options.headers);
this.url = options.url || '';
this._initBody(bodyInit);
}
Body.call(Response.prototype);
Response.prototype.clone = function() {
return new Response(this._bodyInit, {
status: this.status,
statusText: this.statusText,
headers: new Headers(this.headers),
url: this.url
})
};
Response.error = function() {
var response = new Response(null, {status: 200, statusText: ''});
response.ok = false;
response.status = 0;
response.type = 'error';
return response
};
var redirectStatuses = [301, 302, 303, 307, 308];
Response.redirect = function(url, status) {
if (redirectStatuses.indexOf(status) === -1) {
throw new RangeError('Invalid status code')
}
return new Response(null, {status: status, headers: {location: url}})
};
exports.DOMException = g.DOMException;
try {
new exports.DOMException();
} catch (err) {
exports.DOMException = function(message, name) {
this.message = message;
this.name = name;
var error = Error(message);
this.stack = error.stack;
};
exports.DOMException.prototype = Object.create(Error.prototype);
exports.DOMException.prototype.constructor = exports.DOMException;
}
function fetch(input, init) {
return new Promise(function(resolve, reject) {
var request = new Request(input, init);
if (request.signal && request.signal.aborted) {
return reject(new exports.DOMException('Aborted', 'AbortError'))
}
var xhr = new XMLHttpRequest();
function abortXhr() {
xhr.abort();
}
xhr.onload = function() {
var options = {
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders() || '')
};
// This check if specifically for when a user fetches a file locally from the file system
// Only if the status is out of a normal range
if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) {
options.status = 200;
} else {
options.status = xhr.status;
}
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL');
var body = 'response' in xhr ? xhr.response : xhr.responseText;
setTimeout(function() {
resolve(new Response(body, options));
}, 0);
};
xhr.onerror = function() {
setTimeout(function() {
reject(new TypeError('Network request failed'));
}, 0);
};
xhr.ontimeout = function() {
setTimeout(function() {
reject(new TypeError('Network request timed out'));
}, 0);
};
xhr.onabort = function() {
setTimeout(function() {
reject(new exports.DOMException('Aborted', 'AbortError'));
}, 0);
};
function fixUrl(url) {
try {
return url === '' && g.location.href ? g.location.href : url
} catch (e) {
return url
}
}
xhr.open(request.method, fixUrl(request.url), true);
if (request.credentials === 'include') {
xhr.withCredentials = true;
} else if (request.credentials === 'omit') {
xhr.withCredentials = false;
}
if ('responseType' in xhr) {
if (support.blob) {
xhr.responseType = 'blob';
} else if (
support.arrayBuffer
) {
xhr.responseType = 'arraybuffer';
}
}
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
var names = [];
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
names.push(normalizeName(name));
xhr.setRequestHeader(name, normalizeValue(init.headers[name]));
});
request.headers.forEach(function(value, name) {
if (names.indexOf(name) === -1) {
xhr.setRequestHeader(name, value);
}
});
} else {
request.headers.forEach(function(value, name) {
xhr.setRequestHeader(name, value);
});
}
if (request.signal) {
request.signal.addEventListener('abort', abortXhr);
xhr.onreadystatechange = function() {
// DONE (success or failure)
if (xhr.readyState === 4) {
request.signal.removeEventListener('abort', abortXhr);
}
};
}
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit);
})
}
fetch.polyfill = true;
if (!g.fetch) {
g.fetch = fetch;
g.Headers = Headers;
g.Request = Request;
g.Response = Response;
}
exports.Headers = Headers;
exports.Request = Request;
exports.Response = Response;
exports.fetch = fetch;
Object.defineProperty(exports, '__esModule', { value: true });
})));

View File

@@ -1,31 +0,0 @@
// fetch.js code comes from
// https://github.com/JakeChampion/fetch/blob/main/fetch.js
//
// The original code source is available in MIT license.
//
// The script comes from the built version from npm.
// You can get the package with the command:
//
// wget $(npm view whatwg-fetch dist.tarball)
//
// The source is the content of `package/dist/fetch.umd.js` file.
pub const source = @embedFile("fetch.js");
const testing = @import("../../testing.zig");
test "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" },
}, .{});
}

View File

@@ -27,7 +27,6 @@ pub const Loader = struct {
state: enum { empty, loading } = .empty,
done: struct {
fetch: bool = false,
webcomponents: bool = false,
} = .{},
@@ -56,18 +55,6 @@ pub const Loader = struct {
return false;
}
if (!self.done.fetch and isFetch(name)) {
const source = @import("fetch.zig").source;
self.load("fetch", source, js_context);
// We return false here: We want v8 to continue the calling chain
// to finally find the polyfill we just inserted. If we want to
// return false and stops the call chain, we have to use
// `info.GetReturnValue.Set()` function, or `undefined` will be
// returned immediately.
return false;
}
if (!self.done.webcomponents and isWebcomponents(name)) {
const source = @import("webcomponents.zig").source;
self.load("webcomponents", source, js_context);
@@ -89,14 +76,6 @@ pub const Loader = struct {
return false;
}
fn isFetch(name: []const u8) bool {
if (std.mem.eql(u8, name, "fetch")) return true;
if (std.mem.eql(u8, name, "Request")) return true;
if (std.mem.eql(u8, name, "Response")) return true;
if (std.mem.eql(u8, name, "Headers")) return true;
return false;
}
fn isWebcomponents(name: []const u8) bool {
if (std.mem.eql(u8, name, "customElements")) return true;
return false;

View File

@@ -0,0 +1,174 @@
// 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 log = @import("../../log.zig");
const Page = @import("../page.zig").Page;
const Env = @import("../env.zig").Env;
const ReadableStream = @This();
const ReadableStreamDefaultReader = @import("ReadableStreamDefaultReader.zig");
const ReadableStreamDefaultController = @import("ReadableStreamDefaultController.zig");
const State = union(enum) {
readable,
closed: ?[]const u8,
cancelled: ?[]const u8,
errored: Env.JsObject,
};
// This promise resolves when a stream is canceled.
cancel_resolver: Env.PersistentPromiseResolver,
closed_resolver: Env.PersistentPromiseResolver,
reader_resolver: ?Env.PersistentPromiseResolver = null,
locked: bool = false,
state: State = .readable,
cancel_fn: ?Env.Function = null,
pull_fn: ?Env.Function = null,
strategy: QueueingStrategy,
queue: std.ArrayListUnmanaged([]const u8) = .empty,
pub const ReadableStreamReadResult = struct {
const ValueUnion =
union(enum) { data: []const u8, empty: void };
value: ValueUnion,
done: bool,
pub fn get_value(self: *const ReadableStreamReadResult) ValueUnion {
return self.value;
}
pub fn get_done(self: *const ReadableStreamReadResult) bool {
return self.done;
}
};
const UnderlyingSource = struct {
start: ?Env.Function = null,
pull: ?Env.Function = null,
cancel: ?Env.Function = null,
type: ?[]const u8 = null,
};
const QueueingStrategy = struct {
size: ?Env.Function = null,
high_water_mark: u32 = 1,
};
pub fn constructor(underlying: ?UnderlyingSource, _strategy: ?QueueingStrategy, page: *Page) !*ReadableStream {
const strategy: QueueingStrategy = _strategy orelse .{};
const cancel_resolver = try page.main_context.createPersistentPromiseResolver();
const closed_resolver = try page.main_context.createPersistentPromiseResolver();
const stream = try page.arena.create(ReadableStream);
stream.* = ReadableStream{ .cancel_resolver = cancel_resolver, .closed_resolver = closed_resolver, .strategy = strategy };
const controller = ReadableStreamDefaultController{ .stream = stream };
// call start
if (underlying) |src| {
if (src.start) |start| {
try start.call(void, .{controller});
}
if (src.cancel) |cancel| {
stream.cancel_fn = cancel;
}
if (src.pull) |pull| {
stream.pull_fn = pull;
try stream.pullIf();
}
}
return stream;
}
pub fn destructor(self: *ReadableStream) void {
if (self.reader_resolver) |*rr| {
rr.deinit();
}
}
pub fn get_locked(self: *const ReadableStream) bool {
return self.locked;
}
pub fn _cancel(self: *ReadableStream, reason: ?[]const u8, page: *Page) !Env.Promise {
if (self.locked) {
return error.TypeError;
}
self.state = .{ .cancelled = if (reason) |r| try page.arena.dupe(u8, r) else null };
// Call cancel callback.
if (self.cancel_fn) |cancel| {
if (reason) |r| {
try cancel.call(void, .{r});
} else {
try cancel.call(void, .{});
}
}
try self.cancel_resolver.resolve({});
return self.cancel_resolver.promise();
}
pub fn pullIf(self: *ReadableStream) !void {
if (self.pull_fn) |pull_fn| {
// Must be under the high water mark AND readable.
if ((self.queue.items.len < self.strategy.high_water_mark) and self.state == .readable) {
const controller = ReadableStreamDefaultController{ .stream = self };
try pull_fn.call(void, .{controller});
}
}
}
const GetReaderOptions = struct {
// Mode must equal 'byob' or be undefined. RangeError otherwise.
mode: ?[]const u8 = null,
};
pub fn _getReader(self: *ReadableStream, _options: ?GetReaderOptions) !ReadableStreamDefaultReader {
if (self.locked) {
return error.TypeError;
}
// TODO: Determine if we need the ReadableStreamBYOBReader
const options = _options orelse GetReaderOptions{};
_ = options;
return ReadableStreamDefaultReader.constructor(self);
}
// TODO: pipeThrough (requires TransformStream)
// TODO: pipeTo (requires WritableStream)
// TODO: tee
const testing = @import("../../testing.zig");
test "streams: ReadableStream" {
try testing.htmlRunner("streams/readable_stream.html");
}

View File

@@ -0,0 +1,82 @@
// 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 log = @import("../../log.zig");
const Page = @import("../page.zig").Page;
const Env = @import("../env.zig").Env;
const ReadableStream = @import("./ReadableStream.zig");
const ReadableStreamReadResult = @import("./ReadableStream.zig").ReadableStreamReadResult;
const ReadableStreamDefaultController = @This();
stream: *ReadableStream,
pub fn get_desiredSize(self: *const ReadableStreamDefaultController) i32 {
// TODO: This may need tuning at some point if it becomes a performance issue.
return @intCast(self.stream.queue.capacity - self.stream.queue.items.len);
}
pub fn _close(self: *ReadableStreamDefaultController, _reason: ?[]const u8, page: *Page) !void {
const reason = if (_reason) |reason| try page.arena.dupe(u8, reason) else null;
self.stream.state = .{ .closed = reason };
// Resolve the Reader Promise
if (self.stream.reader_resolver) |*rr| {
defer rr.deinit();
try rr.resolve(ReadableStreamReadResult{ .value = .empty, .done = true });
self.stream.reader_resolver = null;
}
// Resolve the Closed promise.
try self.stream.closed_resolver.resolve({});
// close just sets as closed meaning it wont READ any more but anything in the queue is fine to read.
// to discard, must use cancel.
}
pub fn _enqueue(self: *ReadableStreamDefaultController, chunk: []const u8, page: *Page) !void {
const stream = self.stream;
if (stream.state != .readable) {
return error.TypeError;
}
const duped_chunk = try page.arena.dupe(u8, chunk);
if (self.stream.reader_resolver) |*rr| {
defer rr.deinit();
try rr.resolve(ReadableStreamReadResult{ .value = .{ .data = duped_chunk }, .done = false });
self.stream.reader_resolver = null;
}
try self.stream.queue.append(page.arena, duped_chunk);
try self.stream.pullIf();
}
pub fn _error(self: *ReadableStreamDefaultController, err: Env.JsObject) !void {
self.stream.state = .{ .errored = err };
if (self.stream.reader_resolver) |*rr| {
defer rr.deinit();
try rr.reject(err);
self.stream.reader_resolver = null;
}
}

View File

@@ -0,0 +1,96 @@
// 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 log = @import("../../log.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const ReadableStream = @import("./ReadableStream.zig");
const ReadableStreamReadResult = @import("./ReadableStream.zig").ReadableStreamReadResult;
const ReadableStreamDefaultReader = @This();
stream: *ReadableStream,
pub fn constructor(stream: *ReadableStream) ReadableStreamDefaultReader {
return .{ .stream = stream };
}
pub fn get_closed(self: *const ReadableStreamDefaultReader) Env.Promise {
return self.stream.closed_resolver.promise();
}
pub fn _cancel(self: *ReadableStreamDefaultReader, reason: ?[]const u8, page: *Page) !Env.Promise {
return try self.stream._cancel(reason, page);
}
pub fn _read(self: *const ReadableStreamDefaultReader, page: *Page) !Env.Promise {
const stream = self.stream;
switch (stream.state) {
.readable => {
if (stream.queue.items.len > 0) {
const data = self.stream.queue.orderedRemove(0);
const resolver = page.main_context.createPromiseResolver();
try resolver.resolve(ReadableStreamReadResult{ .value = .{ .data = data }, .done = false });
try self.stream.pullIf();
return resolver.promise();
} else {
if (self.stream.reader_resolver) |rr| {
return rr.promise();
} else {
const persistent_resolver = try page.main_context.createPersistentPromiseResolver();
self.stream.reader_resolver = persistent_resolver;
return persistent_resolver.promise();
}
}
},
.closed => |_| {
const resolver = page.main_context.createPromiseResolver();
if (stream.queue.items.len > 0) {
const data = self.stream.queue.orderedRemove(0);
try resolver.resolve(ReadableStreamReadResult{ .value = .{ .data = data }, .done = false });
} else {
try resolver.resolve(ReadableStreamReadResult{ .value = .empty, .done = true });
}
return resolver.promise();
},
.cancelled => |_| {
const resolver = page.main_context.createPromiseResolver();
try resolver.resolve(ReadableStreamReadResult{ .value = .empty, .done = true });
return resolver.promise();
},
.errored => |err| {
const resolver = page.main_context.createPromiseResolver();
try resolver.reject(err);
return resolver.promise();
},
}
}
pub fn _releaseLock(self: *const ReadableStreamDefaultReader) !void {
self.stream.locked = false;
if (self.stream.reader_resolver) |rr| {
try rr.reject("TypeError");
}
}

View File

@@ -0,0 +1,24 @@
// 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/>.
pub const Interfaces = .{
@import("ReadableStream.zig"),
@import("ReadableStream.zig").ReadableStreamReadResult,
@import("ReadableStreamDefaultReader.zig"),
@import("ReadableStreamDefaultController.zig"),
};

View File

@@ -200,6 +200,7 @@ pub fn requestIntercept(arena: Allocator, bc: anytype, intercept: *const Notific
.script => "Script",
.xhr => "XHR",
.document => "Document",
.fetch => "Fetch",
},
.networkId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}),
}, .{ .session_id = session_id });
@@ -405,6 +406,7 @@ pub fn requestAuthRequired(arena: Allocator, bc: anytype, intercept: *const Noti
.script => "Script",
.xhr => "XHR",
.document => "Document",
.fetch => "Fetch",
},
.authChallenge = .{
.source = if (challenge.source == .server) "Server" else "Proxy",

View File

@@ -649,6 +649,7 @@ pub const Request = struct {
document,
xhr,
script,
fetch,
};
};

View File

@@ -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.

View File

@@ -39,6 +39,7 @@ pub const Scope = enum {
unknown_prop,
web_api,
xhr,
fetch,
polyfill,
mouse_event,
};

View File

@@ -677,6 +677,9 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
// we now simply persist every time persist() is called.
js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty,
persisted_promise_resolvers: std.ArrayListUnmanaged(v8.Persistent(v8.PromiseResolver)) = .empty,
// When we need to load a resource (i.e. an external script), we call
// this function to get the source. This is always a reference to the
// Page's fetchModuleSource, but we use a function pointer
@@ -733,6 +736,10 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
p.deinit();
}
for (self.persisted_promise_resolvers.items) |*p| {
p.deinit();
}
{
var it = self.module_cache.valueIterator();
while (it.next()) |p| {
@@ -1130,6 +1137,16 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
},
else => {},
},
.array => |arr| {
// Retrieve fixed-size array as slice
const slice_type = []arr.child;
const slice_value = try self.jsValueToZig(named_function, slice_type, js_value);
if (slice_value.len != arr.len) {
// Exact length match, we could allow smaller arrays, but we would not be able to communicate how many were written
return error.InvalidArgument;
}
return @as(*T, @ptrCast(slice_value.ptr)).*;
},
.@"struct" => {
return try (self.jsValueToStruct(named_function, T, js_value)) orelse {
return error.InvalidArgument;
@@ -1251,6 +1268,15 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
};
}
pub fn createPersistentPromiseResolver(self: *JsContext) !PersistentPromiseResolver {
const resolver = v8.Persistent(v8.PromiseResolver).init(self.isolate, v8.PromiseResolver.init(self.v8_context));
try self.persisted_promise_resolvers.append(self.context_arena, resolver);
return .{
.js_context = self,
.resolver = resolver,
};
}
// Probing is part of trying to map a JS value to a Zig union. There's
// a lot of ambiguity in this process, in part because some JS values
// can almost always be coerced. For example, anything can be coerced
@@ -1413,6 +1439,36 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
},
else => {},
},
.array => |arr| {
// Retrieve fixed-size array as slice then probe
const slice_type = []arr.child;
switch (try self.probeJsValueToZig(named_function, slice_type, js_value)) {
.value => |slice_value| {
if (slice_value.len == arr.len) {
return .{ .value = @as(*T, @ptrCast(slice_value.ptr)).* };
}
return .{ .invalid = {} };
},
.ok => {
// Exact length match, we could allow smaller arrays as .compatible, but we would not be able to communicate how many were written
if (js_value.isArray()) {
const js_arr = js_value.castTo(v8.Array);
if (js_arr.length() == arr.len) {
return .{ .ok = {} };
}
} else if (js_value.isString() and arr.child == u8) {
const str = try js_value.toString(self.v8_context);
if (str.lenUtf8(self.isolate) == arr.len) {
return .{ .ok = {} };
}
}
return .{ .invalid = {} };
},
.compatible => return .{ .compatible = {} },
.coerce => return .{ .coerce = {} },
.invalid => return .{ .invalid = {} },
}
},
.@"struct" => {
// We don't want to duplicate the code for this, so we call
// the actual conversion function.
@@ -2178,6 +2234,54 @@ 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 PersistentPromiseResolver = struct {
js_context: *JsContext,
resolver: v8.Persistent(v8.PromiseResolver),
pub fn deinit(self: *PersistentPromiseResolver) void {
self.resolver.deinit();
}
pub fn promise(self: PersistentPromiseResolver) Promise {
return .{
.promise = self.resolver.castToPromiseResolver().getPromise(),
};
}
pub fn resolve(self: PersistentPromiseResolver, value: anytype) !void {
const js_context = self.js_context;
const js_value = try js_context.zigValueToJs(value);
// resolver.resolve will return null if the promise isn't pending
const ok = self.resolver.castToPromiseResolver().resolve(js_context.v8_context, js_value) orelse return;
if (!ok) {
return error.FailedToResolvePromise;
}
}
pub fn reject(self: PersistentPromiseResolver, 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.castToPromiseResolver().reject(js_context.v8_context, js_value) orelse return;
if (!ok) {
return error.FailedToRejectPromise;
}
}
};
pub const Promise = struct {

View File

@@ -0,0 +1,16 @@
<script src="../testing.js"></script>
<script id=fetch type=module>
const promise1 = new Promise((resolve) => {
fetch('http://127.0.0.1:9582/xhr/json')
.then((res) => {
return res.json()
})
.then((json) => {
resolve(json);
});
});
testing.async(promise1, (json) => {
testing.expectEqual({over: '9000!!!'}, json);
});
</script>

View File

@@ -0,0 +1,102 @@
<script src="../testing.js"></script>
<script id=headers>
let headers = new Headers({"Set-Cookie": "name=world"});
testing.expectEqual("name=world", headers.get("set-cookie"));
let myHeaders = new Headers();
myHeaders.append("Content-Type", "image/jpeg"),
testing.expectEqual(false, myHeaders.has("Picture-Type"));
testing.expectEqual("image/jpeg", myHeaders.get("Content-Type"));
myHeaders.append("Content-Type", "image/png");
testing.expectEqual("image/jpeg, image/png", myHeaders.get("Content-Type"));
myHeaders.delete("Content-Type");
testing.expectEqual(null, myHeaders.get("Content-Type"));
myHeaders.set("Picture-Type", "image/svg")
testing.expectEqual("image/svg", myHeaders.get("Picture-Type"));
testing.expectEqual(true, myHeaders.has("Picture-Type"))
const originalHeaders = new Headers([["Content-Type", "application/json"], ["Authorization", "Bearer token123"]]);
testing.expectEqual("application/json", originalHeaders.get("Content-Type"));
testing.expectEqual("Bearer token123", originalHeaders.get("Authorization"));
const newHeaders = new Headers(originalHeaders);
testing.expectEqual("application/json", newHeaders.get("Content-Type"));
testing.expectEqual("Bearer token123" ,newHeaders.get("Authorization"));
testing.expectEqual(true ,newHeaders.has("Content-Type"));
testing.expectEqual(true ,newHeaders.has("Authorization"));
testing.expectEqual(false, newHeaders.has("X-Custom"));
newHeaders.set("X-Custom", "test-value");
testing.expectEqual("test-value", newHeaders.get("X-Custom"));
testing.expectEqual(null, originalHeaders.get("X-Custom"));
testing.expectEqual(false, originalHeaders.has("X-Custom"));
</script>
<script id=keys>
const testKeyHeaders = new Headers();
testKeyHeaders.set("Content-Type", "application/json");
testKeyHeaders.set("Authorization", "Bearer token123");
testKeyHeaders.set("X-Custom", "test-value");
const keys = [];
for (const key of testKeyHeaders.keys()) {
keys.push(key);
}
testing.expectEqual(3, keys.length);
testing.expectEqual(true, keys.includes("Content-Type"));
testing.expectEqual(true, keys.includes("Authorization"));
testing.expectEqual(true, keys.includes("X-Custom"));
</script>
<script id=values>
const testValuesHeaders = new Headers();
testValuesHeaders.set("Content-Type", "application/json");
testValuesHeaders.set("Authorization", "Bearer token123");
testValuesHeaders.set("X-Custom", "test-value");
const values = [];
for (const value of testValuesHeaders.values()) {
values.push(value);
}
testing.expectEqual(3, values.length);
testing.expectEqual(true, values.includes("application/json"));
testing.expectEqual(true, values.includes("Bearer token123"));
testing.expectEqual(true, values.includes("test-value"));
</script>
<script id=entries>
const testEntriesHeaders = new Headers();
testEntriesHeaders.set("Content-Type", "application/json");
testEntriesHeaders.set("Authorization", "Bearer token123");
testEntriesHeaders.set("X-Custom", "test-value");
const entries = [];
for (const entry of testEntriesHeaders.entries()) {
entries.push(entry);
}
testing.expectEqual(3, entries.length);
const entryMap = new Map(entries);
testing.expectEqual("application/json", entryMap.get("Content-Type"));
testing.expectEqual("Bearer token123", entryMap.get("Authorization"));
testing.expectEqual("test-value", entryMap.get("X-Custom"));
const entryKeys = Array.from(entryMap.keys());
testing.expectEqual(3, entryKeys.length);
testing.expectEqual(true, entryKeys.includes("Content-Type"));
testing.expectEqual(true, entryKeys.includes("Authorization"));
testing.expectEqual(true, entryKeys.includes("X-Custom"));
const entryValues = Array.from(entryMap.values());
testing.expectEqual(3, entryValues.length);
testing.expectEqual(true, entryValues.includes("application/json"));
testing.expectEqual(true, entryValues.includes("Bearer token123"));
testing.expectEqual(true, entryValues.includes("test-value"))
</script>

View File

@@ -0,0 +1,22 @@
<script src="../testing.js"></script>
<script id=request>
let request = new Request("flower.png");
testing.expectEqual("http://localhost:9582/src/tests/fetch/flower.png", request.url);
testing.expectEqual("GET", request.method);
let request2 = new Request("https://google.com", {
method: "POST",
body: "Hello, World",
cache: "reload",
credentials: "omit",
headers: { "Sender": "me", "Target": "you" }
}
);
testing.expectEqual("https://google.com", request2.url);
testing.expectEqual("POST", request2.method);
testing.expectEqual("omit", request2.credentials);
testing.expectEqual("reload", request2.cache);
testing.expectEqual("me", request2.headers.get("SeNdEr"));
testing.expectEqual("you", request2.headers.get("target"));
</script>

View File

@@ -0,0 +1,49 @@
<script src="../testing.js"></script>
<script id=response>
let response = new Response("Hello, World!");
testing.expectEqual(200, response.status);
testing.expectEqual("", response.statusText);
testing.expectEqual(true, response.ok);
testing.expectEqual("", response.url);
testing.expectEqual(false, response.redirected);
let response2 = new Response("Error occurred", {
status: 404,
statusText: "Not Found",
headers: {
"Content-Type": "text/plain",
"X-Custom": "test-value",
"Cache-Control": "no-cache"
}
});
testing.expectEqual(404, response2.status);
testing.expectEqual("Not Found", response2.statusText);
testing.expectEqual(false, response2.ok);
testing.expectEqual("text/plain", response2.headers.get("Content-Type"));
testing.expectEqual("test-value", response2.headers.get("X-Custom"));
testing.expectEqual("no-cache", response2.headers.get("cache-control"));
let response3 = new Response("Created", { status: 201, statusText: "Created" });
testing.expectEqual(201, response3.status);
testing.expectEqual("Created", response3.statusText);
testing.expectEqual(true, response3.ok);
let nullResponse = new Response(null);
testing.expectEqual(200, nullResponse.status);
testing.expectEqual("", nullResponse.statusText);
let emptyResponse = new Response("");
testing.expectEqual(200, emptyResponse.status);
</script>
<script id=json type=module>
const promise1 = new Promise((resolve) => {
let response = new Response('[]');
response.json().then(resolve)
});
testing.async(promise1, (json) => {
testing.expectEqual([], json);
});
</script>

View File

@@ -0,0 +1,66 @@
<script src="../testing.js"></script>
<script>
class LightPanda extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
const shadow = this.attachShadow({ mode: "open" });
const slot1 = document.createElement('slot');
slot1.name = 'slot-1';
shadow.appendChild(slot1);
switch (this.getAttribute('mode')) {
case '1':
slot1.innerHTML = 'hello';
break;
case '2':
const slot2 = document.createElement('slot');
shadow.appendChild(slot2);
break;
}
}
}
window.customElements.define("lp-test", LightPanda);
</script>
<lp-test id=lp1 mode=1></lp-test>
<lp-test id=lp2 mode=0></lp-test>
<lp-test id=lp3 mode=0>default</lp-test>
<lp-test id=lp4 mode=1><p slot=other>default</p></lp-test>
<lp-test id=lp5 mode=1><p slot=slot-1>default</p> xx <b slot=slot-1>other</b></lp-test>
<lp-test id=lp6 mode=2>More <p slot=slot-1>default2</p> <span>!!</span></lp-test>
<script id=HTMLSlotElement>
function assertNodes(expected, actual) {
actual = actual.map((n) => n.id || n.textContent)
testing.expectEqual(expected, actual);
}
for (let idx of [1, 2, 3, 4]) {
const lp = $(`#lp${idx}`);
const slot = lp.shadowRoot.querySelector('slot');
assertNodes([], slot.assignedNodes());
assertNodes([], slot.assignedNodes({}));
assertNodes([], slot.assignedNodes({flatten: false}));
if (lp.getAttribute('mode') === '1') {
assertNodes(['hello'], slot.assignedNodes({flatten: true}));
} else {
assertNodes([], slot.assignedNodes({flatten: true}));
}
}
const lp5 = $('#lp5');
const s5 = lp5.shadowRoot.querySelector('slot');
assertNodes(['default', 'other'], s5.assignedNodes());
const lp6 = $('#lp6');
const s6 = lp6.shadowRoot.querySelectorAll('slot');
assertNodes(['default2'], s6[0].assignedNodes({}));
assertNodes(['default2'], s6[0].assignedNodes({flatten: true}));
assertNodes(['More ', ' ', '!!'], s6[1].assignedNodes({}));
assertNodes(['More ', ' ', '!!'], s6[1].assignedNodes({flatten: true}));
</script>

View File

@@ -0,0 +1,117 @@
<script src="../testing.js"></script>
<script id=readable_stream>
const stream = new ReadableStream({
start(controller) {
controller.enqueue("hello");
controller.enqueue("world");
controller.close();
}
});
const reader = stream.getReader();
testing.async(reader.read(), (data) => {
testing.expectEqual("hello", data.value);
testing.expectEqual(false, data.done);
});
</script>
<script id=readable_stream_close>
var closeResult;
const stream1 = new ReadableStream({
start(controller) {
controller.enqueue("first");
controller.enqueue("second");
controller.close();
}
});
const reader1 = stream1.getReader();
testing.async(reader1.read(), (data) => {
testing.expectEqual("first", data.value);
testing.expectEqual(false, data.done);
});
testing.async(reader1.read(), (data) => {
testing.expectEqual("second", data.value);
testing.expectEqual(false, data.done);
});
testing.async(reader1.read(), (data) => {
testing.expectEqual(undefined, data.value);
testing.expectEqual(true, data.done);
});
</script>
<script id=readable_stream_cancel>
var readResult;
var cancelResult;
var closeResult;
const stream2 = new ReadableStream({
start(controller) {
controller.enqueue("data1");
controller.enqueue("data2");
controller.enqueue("data3");
},
cancel(reason) {
closeResult = `Stream cancelled: ${reason}`;
}
});
const reader2 = stream2.getReader();
testing.async(reader2.read(), (data) => {
testing.expectEqual("data1", data.value);
testing.expectEqual(false, data.done);
});
testing.async(reader2.cancel("user requested"), (result) => {
testing.expectEqual(undefined, result);
testing.expectEqual("Stream cancelled: user requested", closeResult);
});
</script>
<script id=readable_stream_cancel_no_reason>
var closeResult2;
const stream3 = new ReadableStream({
start(controller) {
controller.enqueue("test");
},
cancel(reason) {
closeResult2 = reason === undefined ? "no reason" : reason;
}
});
const reader3 = stream3.getReader();
testing.async(reader3.cancel(), (result) => {
testing.expectEqual(undefined, result);
testing.expectEqual("no reason", closeResult2);
});
</script>
<script id=readable_stream_read_after_cancel>
var readAfterCancelResult;
const stream4 = new ReadableStream({
start(controller) {
controller.enqueue("before_cancel");
},
});
const reader4 = stream4.getReader();
testing.async(reader4.cancel("test cancel"), (cancelResult) => {
testing.expectEqual(undefined, cancelResult);
});
testing.async(reader4.read(), (data) => {
testing.expectEqual(undefined, data.value);
testing.expectEqual(true, data.done);
});
</script>