mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-17 00:38:59 +00:00
Merge pull request #972 from lightpanda-io/fetch
Fetch + ReadableStream
This commit is contained in:
@@ -36,6 +36,8 @@ 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/fetch.zig").Interfaces,
|
||||||
|
@import("streams/streams.zig").Interfaces,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
227
src/browser/fetch/Headers.zig
Normal file
227
src/browser/fetch/Headers.zig
Normal 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");
|
||||||
|
}
|
||||||
266
src/browser/fetch/Request.zig
Normal file
266
src/browser/fetch/Request.zig
Normal 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");
|
||||||
|
}
|
||||||
196
src/browser/fetch/Response.zig
Normal file
196
src/browser/fetch/Response.zig
Normal 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
212
src/browser/fetch/fetch.zig
Normal 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(.page);
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
@@ -39,6 +39,10 @@ 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 fetchFn = @import("../fetch/fetch.zig").fetch;
|
||||||
|
|
||||||
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 +99,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 fetchFn(input, options, page);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_window(self: *Window) *Window {
|
pub fn get_window(self: *Window) *Window {
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
|
||||||
|
|
||||||
})));
|
|
||||||
@@ -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" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,6 @@ pub const Loader = struct {
|
|||||||
state: enum { empty, loading } = .empty,
|
state: enum { empty, loading } = .empty,
|
||||||
|
|
||||||
done: struct {
|
done: struct {
|
||||||
fetch: bool = false,
|
|
||||||
webcomponents: bool = false,
|
webcomponents: bool = false,
|
||||||
} = .{},
|
} = .{},
|
||||||
|
|
||||||
@@ -56,18 +55,6 @@ pub const Loader = struct {
|
|||||||
return false;
|
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)) {
|
if (!self.done.webcomponents and isWebcomponents(name)) {
|
||||||
const source = @import("webcomponents.zig").source;
|
const source = @import("webcomponents.zig").source;
|
||||||
self.load("webcomponents", source, js_context);
|
self.load("webcomponents", source, js_context);
|
||||||
@@ -89,14 +76,6 @@ pub const Loader = struct {
|
|||||||
return false;
|
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 {
|
fn isWebcomponents(name: []const u8) bool {
|
||||||
if (std.mem.eql(u8, name, "customElements")) return true;
|
if (std.mem.eql(u8, name, "customElements")) return true;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
178
src/browser/streams/ReadableStream.zig
Normal file
178
src/browser/streams/ReadableStream.zig
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
// 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(.self);
|
||||||
|
const closed_resolver = try page.main_context.createPersistentPromiseResolver(.self);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
self.cancel_resolver.deinit();
|
||||||
|
self.closed_resolver.deinit();
|
||||||
|
// reader resolver is scoped to the page lifetime and is cleaned up by it.
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
79
src/browser/streams/ReadableStreamDefaultController.zig
Normal file
79
src/browser/streams/ReadableStreamDefaultController.zig
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// 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| {
|
||||||
|
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| {
|
||||||
|
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| {
|
||||||
|
try rr.reject(err);
|
||||||
|
self.stream.reader_resolver = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
96
src/browser/streams/ReadableStreamDefaultReader.zig
Normal file
96
src/browser/streams/ReadableStreamDefaultReader.zig
Normal 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(.page);
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/browser/streams/streams.zig
Normal file
24
src/browser/streams/streams.zig
Normal 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"),
|
||||||
|
};
|
||||||
@@ -200,6 +200,7 @@ pub fn requestIntercept(arena: Allocator, bc: anytype, intercept: *const Notific
|
|||||||
.script => "Script",
|
.script => "Script",
|
||||||
.xhr => "XHR",
|
.xhr => "XHR",
|
||||||
.document => "Document",
|
.document => "Document",
|
||||||
|
.fetch => "Fetch",
|
||||||
},
|
},
|
||||||
.networkId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}),
|
.networkId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}),
|
||||||
}, .{ .session_id = session_id });
|
}, .{ .session_id = session_id });
|
||||||
@@ -405,6 +406,7 @@ pub fn requestAuthRequired(arena: Allocator, bc: anytype, intercept: *const Noti
|
|||||||
.script => "Script",
|
.script => "Script",
|
||||||
.xhr => "XHR",
|
.xhr => "XHR",
|
||||||
.document => "Document",
|
.document => "Document",
|
||||||
|
.fetch => "Fetch",
|
||||||
},
|
},
|
||||||
.authChallenge = .{
|
.authChallenge = .{
|
||||||
.source = if (challenge.source == .server) "Server" else "Proxy",
|
.source = if (challenge.source == .server) "Server" else "Proxy",
|
||||||
|
|||||||
@@ -657,6 +657,7 @@ pub const Request = struct {
|
|||||||
document,
|
document,
|
||||||
xhr,
|
xhr,
|
||||||
script,
|
script,
|
||||||
|
fetch,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -346,13 +346,13 @@ pub const Opts = struct {
|
|||||||
user_agent: [:0]const u8,
|
user_agent: [:0]const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ pub const Scope = enum {
|
|||||||
unknown_prop,
|
unknown_prop,
|
||||||
web_api,
|
web_api,
|
||||||
xhr,
|
xhr,
|
||||||
|
fetch,
|
||||||
polyfill,
|
polyfill,
|
||||||
mouse_event,
|
mouse_event,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -677,6 +677,11 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
|||||||
// we now simply persist every time persist() is called.
|
// we now simply persist every time persist() is called.
|
||||||
js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty,
|
js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty,
|
||||||
|
|
||||||
|
// Various web APIs depend on having a persistent promise resolver. They
|
||||||
|
// require for this PromiseResolver to be valid for a lifetime longer than
|
||||||
|
// the function that resolves/rejects them.
|
||||||
|
persisted_promise_resolvers: std.ArrayListUnmanaged(v8.Persistent(v8.PromiseResolver)) = .empty,
|
||||||
|
|
||||||
// When we need to load a resource (i.e. an external script), we call
|
// 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
|
// this function to get the source. This is always a reference to the
|
||||||
// Page's fetchModuleSource, but we use a function pointer
|
// Page's fetchModuleSource, but we use a function pointer
|
||||||
@@ -733,6 +738,10 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
|||||||
p.deinit();
|
p.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (self.persisted_promise_resolvers.items) |*p| {
|
||||||
|
p.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
var it = self.module_cache.valueIterator();
|
var it = self.module_cache.valueIterator();
|
||||||
while (it.next()) |p| {
|
while (it.next()) |p| {
|
||||||
@@ -1130,6 +1139,16 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
|||||||
},
|
},
|
||||||
else => {},
|
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" => {
|
.@"struct" => {
|
||||||
return try (self.jsValueToStruct(named_function, T, js_value)) orelse {
|
return try (self.jsValueToStruct(named_function, T, js_value)) orelse {
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
@@ -1251,6 +1270,22 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// creates a PersistentPromiseResolver, taking in a lifetime parameter.
|
||||||
|
// If the lifetime is page, the page will clean up the PersistentPromiseResolver.
|
||||||
|
// If the lifetime is self, you will be expected to deinitalize the PersistentPromiseResolver.
|
||||||
|
pub fn createPersistentPromiseResolver(
|
||||||
|
self: *JsContext,
|
||||||
|
lifetime: enum { self, page },
|
||||||
|
) !PersistentPromiseResolver {
|
||||||
|
const resolver = v8.Persistent(v8.PromiseResolver).init(self.isolate, v8.PromiseResolver.init(self.v8_context));
|
||||||
|
|
||||||
|
if (lifetime == .page) {
|
||||||
|
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
|
// 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
|
// a lot of ambiguity in this process, in part because some JS values
|
||||||
// can almost always be coerced. For example, anything can be coerced
|
// can almost always be coerced. For example, anything can be coerced
|
||||||
@@ -1413,6 +1448,36 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
|||||||
},
|
},
|
||||||
else => {},
|
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" => {
|
.@"struct" => {
|
||||||
// We don't want to duplicate the code for this, so we call
|
// We don't want to duplicate the code for this, so we call
|
||||||
// the actual conversion function.
|
// the actual conversion function.
|
||||||
@@ -2178,6 +2243,54 @@ 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 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 {
|
pub const Promise = struct {
|
||||||
|
|||||||
16
src/tests/fetch/fetch.html
Normal file
16
src/tests/fetch/fetch.html
Normal 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>
|
||||||
102
src/tests/fetch/headers.html
Normal file
102
src/tests/fetch/headers.html
Normal 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>
|
||||||
22
src/tests/fetch/request.html
Normal file
22
src/tests/fetch/request.html
Normal 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>
|
||||||
49
src/tests/fetch/response.html
Normal file
49
src/tests/fetch/response.html
Normal 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>
|
||||||
117
src/tests/streams/readable_stream.html
Normal file
117
src/tests/streams/readable_stream.html
Normal 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>
|
||||||
Reference in New Issue
Block a user