mirror of
				https://github.com/lightpanda-io/browser.git
				synced 2025-10-29 23:23:28 +00:00 
			
		
		
		
	Compare commits
	
		
			43 Commits
		
	
	
		
			55e9d8d166
			...
			fetch_set_
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 7d96a25c55 | ||
|   | 4d1e416299 | ||
|   | 3badcdbdbd | ||
|   | fcd82b2c14 | ||
|   | d0621510cc | ||
|   | 2a7a8bc2a6 | ||
|   | af916dea1d | ||
|   | 31335fc4fb | ||
|   | c84634093d | ||
|   | 37d8d2642d | ||
|   | 0423a178e9 | ||
|   | 7acf67d668 | ||
|   | ef1fece40c | ||
|   | ebb590250f | ||
|   | 03130a95d8 | ||
|   | e133717f7f | ||
|   | 968c695da1 | ||
|   | 707116a030 | ||
|   | 01966f41ff | ||
|   | 141d17dd55 | ||
|   | a3c2daf306 | ||
|   | dc60fac90d | ||
|   | a5e2e8ea15 | ||
|   | 8295c2abe5 | ||
|   | 5997be89f6 | ||
|   | 1c89cfe5d4 | ||
|   | b5021bd9fa | ||
|   | 4fd365b520 | ||
|   | 479cd5ab1a | ||
|   | 8285cbcaa9 | ||
|   | 545d97b5c0 | ||
|   | 11016abdd3 | ||
|   | 066df87dd4 | ||
|   | 91899912d8 | ||
|   | 4ceca6b90b | ||
|   | ec936417c6 | ||
|   | 4b75b33eb3 | ||
|   | 1d7e731034 | ||
|   | ab60f64452 | ||
|   | 9757ea7b0f | ||
|   | 855583874f | ||
|   | 9efc27c2bb | ||
|   | cab5117d85 | 
| @@ -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"); | ||||||
|  | } | ||||||
							
								
								
									
										214
									
								
								src/browser/fetch/fetch.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								src/browser/fetch/fetch.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,214 @@ | |||||||
|  | // 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 = page.main_context.createPersistentPromiseResolver(); | ||||||
|  |  | ||||||
|  |     const fetch_ctx = try arena.create(FetchContext); | ||||||
|  |     fetch_ctx.* = .{ | ||||||
|  |         .arena = arena, | ||||||
|  |         .js_ctx = page.main_context, | ||||||
|  |         .promise_resolver = resolver, | ||||||
|  |         .method = req.method, | ||||||
|  |         .url = req.url, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     try page.http_client.request(.{ | ||||||
|  |         .ctx = @ptrCast(fetch_ctx), | ||||||
|  |         .url = req.url, | ||||||
|  |         .method = req.method, | ||||||
|  |         .headers = headers, | ||||||
|  |         .body = req.body, | ||||||
|  |         .cookie_jar = page.cookie_jar, | ||||||
|  |         .resource_type = .fetch, | ||||||
|  |  | ||||||
|  |         .start_callback = struct { | ||||||
|  |             fn startCallback(transfer: *HttpClient.Transfer) !void { | ||||||
|  |                 const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx)); | ||||||
|  |                 log.debug(.fetch, "request start", .{ .method = self.method, .url = self.url, .source = "fetch" }); | ||||||
|  |  | ||||||
|  |                 self.transfer = transfer; | ||||||
|  |             } | ||||||
|  |         }.startCallback, | ||||||
|  |         .header_callback = struct { | ||||||
|  |             fn headerCallback(transfer: *HttpClient.Transfer) !void { | ||||||
|  |                 const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx)); | ||||||
|  |  | ||||||
|  |                 const header = &transfer.response_header.?; | ||||||
|  |  | ||||||
|  |                 log.debug(.fetch, "request header", .{ | ||||||
|  |                     .source = "fetch", | ||||||
|  |                     .method = self.method, | ||||||
|  |                     .url = self.url, | ||||||
|  |                     .status = header.status, | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |                 if (header.contentType()) |ct| { | ||||||
|  |                     self.mime = Mime.parse(ct) catch { | ||||||
|  |                         return error.MimeParsing; | ||||||
|  |                     }; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 if (transfer.getContentLength()) |cl| { | ||||||
|  |                     try self.body.ensureTotalCapacity(self.arena, cl); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 var it = transfer.responseHeaderIterator(); | ||||||
|  |                 while (it.next()) |hdr| { | ||||||
|  |                     const joined = try std.fmt.allocPrint(self.arena, "{s}: {s}", .{ hdr.name, hdr.value }); | ||||||
|  |                     try self.headers.append(self.arena, joined); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 self.status = header.status; | ||||||
|  |             } | ||||||
|  |         }.headerCallback, | ||||||
|  |         .data_callback = struct { | ||||||
|  |             fn dataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void { | ||||||
|  |                 const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx)); | ||||||
|  |                 try self.body.appendSlice(self.arena, data); | ||||||
|  |             } | ||||||
|  |         }.dataCallback, | ||||||
|  |         .done_callback = struct { | ||||||
|  |             fn doneCallback(ctx: *anyopaque) !void { | ||||||
|  |                 const self: *FetchContext = @ptrCast(@alignCast(ctx)); | ||||||
|  |                 defer self.promise_resolver.setWeak(); | ||||||
|  |                 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)); | ||||||
|  |                 defer self.promise_resolver.setWeak(); | ||||||
|  |                 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"); | ||||||
|  | } | ||||||
| @@ -1042,84 +1042,68 @@ pub const HTMLSlotElement = struct { | |||||||
|         flatten: bool = false, |         flatten: bool = false, | ||||||
|     }; |     }; | ||||||
|     pub fn _assignedNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion { |     pub fn _assignedNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion { | ||||||
|         return findAssignedSlotNodes(self, opts_, false, page); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // This should return Union, instead of NodeUnion, but we want to re-use |  | ||||||
|     // findAssignedSlotNodes. Returning NodeUnion is fine, as long as every element |  | ||||||
|     // within is an Element. This could be more efficient |  | ||||||
|     pub fn _assignedElements(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion { |  | ||||||
|         return findAssignedSlotNodes(self, opts_, true, page); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fn findAssignedSlotNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, element_only: bool, page: *Page) ![]NodeUnion { |  | ||||||
|         const opts = opts_ orelse AssignedNodesOpts{ .flatten = false }; |         const opts = opts_ orelse AssignedNodesOpts{ .flatten = false }; | ||||||
|  |  | ||||||
|         if (opts.flatten) { |         if (try findAssignedSlotNodes(self, opts, page)) |nodes| { | ||||||
|             log.debug(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" }); |             return nodes; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!opts.flatten) { | ||||||
|  |             return &.{}; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const node: *parser.Node = @ptrCast(@alignCast(self)); |         const node: *parser.Node = @ptrCast(@alignCast(self)); | ||||||
|  |         const nl = try parser.nodeGetChildNodes(node); | ||||||
|  |         const len = try parser.nodeListLength(nl); | ||||||
|  |         if (len == 0) { | ||||||
|  |             return &.{}; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         // First we look for any explicitly assigned nodes (via the slot attribute) |         var assigned = try page.call_arena.alloc(NodeUnion, len); | ||||||
|         { |         var i: usize = 0; | ||||||
|             const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name"); |         while (true) : (i += 1) { | ||||||
|             var root = try parser.nodeGetRootNode(node); |             const child = try parser.nodeListItem(nl, @intCast(i)) orelse break; | ||||||
|             if (page.getNodeState(root)) |state| { |             assigned[i] = try Node.toInterface(child); | ||||||
|                 if (state.shadow_root) |sr| { |         } | ||||||
|                     root = @ptrCast(@alignCast(sr.host)); |         return assigned[0..i]; | ||||||
|                 } |     } | ||||||
|  |  | ||||||
|  |     fn findAssignedSlotNodes(self: *parser.Slot, opts: AssignedNodesOpts, page: *Page) !?[]NodeUnion { | ||||||
|  |         if (opts.flatten) { | ||||||
|  |             log.warn(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name"); | ||||||
|  |         const node: *parser.Node = @ptrCast(@alignCast(self)); | ||||||
|  |         var root = try parser.nodeGetRootNode(node); | ||||||
|  |         if (page.getNodeState(root)) |state| { | ||||||
|  |             if (state.shadow_root) |sr| { | ||||||
|  |                 root = @ptrCast(@alignCast(sr.host)); | ||||||
|             } |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|             var arr: std.ArrayList(NodeUnion) = .empty; |         var arr: std.ArrayList(NodeUnion) = .empty; | ||||||
|             const w = @import("../dom/walker.zig").WalkerChildren{}; |         const w = @import("../dom/walker.zig").WalkerChildren{}; | ||||||
|             var next: ?*parser.Node = null; |         var next: ?*parser.Node = null; | ||||||
|             while (true) { |         while (true) { | ||||||
|                 next = try w.get_next(root, next) orelse break; |             next = try w.get_next(root, next) orelse break; | ||||||
|                 if (try parser.nodeType(next.?) != .element) { |             if (try parser.nodeType(next.?) != .element) { | ||||||
|                     if (slot_name == null and !element_only) { |                 if (slot_name == null) { | ||||||
|                         // default slot (with no name), takes everything |                     // default slot (with no name), takes everything | ||||||
|                         try arr.append(page.call_arena, try Node.toInterface(next.?)); |  | ||||||
|                     } |  | ||||||
|                     continue; |  | ||||||
|                 } |  | ||||||
|                 const el: *parser.Element = @ptrCast(@alignCast(next.?)); |  | ||||||
|                 const element_slot = try parser.elementGetAttribute(el, "slot"); |  | ||||||
|  |  | ||||||
|                 if (nullableStringsAreEqual(slot_name, element_slot)) { |  | ||||||
|                     // either they're the same string or they are both null |  | ||||||
|                     try arr.append(page.call_arena, try Node.toInterface(next.?)); |                     try arr.append(page.call_arena, try Node.toInterface(next.?)); | ||||||
|                     continue; |  | ||||||
|                 } |                 } | ||||||
|  |                 continue; | ||||||
|             } |             } | ||||||
|             if (arr.items.len > 0) { |             const el: *parser.Element = @ptrCast(@alignCast(next.?)); | ||||||
|                 return arr.items; |             const element_slot = try parser.elementGetAttribute(el, "slot"); | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (!opts.flatten) { |             if (nullableStringsAreEqual(slot_name, element_slot)) { | ||||||
|                 return &.{}; |                 // either they're the same string or they are both null | ||||||
|  |                 try arr.append(page.call_arena, try Node.toInterface(next.?)); | ||||||
|  |                 continue; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         return if (arr.items.len == 0) null else arr.items; | ||||||
|         // Since, we have no explicitly assigned nodes and flatten == false, |  | ||||||
|         // we'll collect the children of the slot - the defaults. |  | ||||||
|         { |  | ||||||
|             const nl = try parser.nodeGetChildNodes(node); |  | ||||||
|             const len = try parser.nodeListLength(nl); |  | ||||||
|             if (len == 0) { |  | ||||||
|                 return &.{}; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             var assigned = try page.call_arena.alloc(NodeUnion, len); |  | ||||||
|             var i: usize = 0; |  | ||||||
|             while (true) : (i += 1) { |  | ||||||
|                 const child = try parser.nodeListItem(nl, @intCast(i)) orelse break; |  | ||||||
|                 if (!element_only or try parser.nodeType(child) == .element) { |  | ||||||
|                     assigned[i] = try Node.toInterface(child); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             return assigned[0..i]; |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fn nullableStringsAreEqual(a: ?[]const u8, b: ?[]const u8) bool { |     fn nullableStringsAreEqual(a: ?[]const u8, b: ?[]const u8) bool { | ||||||
| @@ -1345,6 +1329,39 @@ test "Browser: HTML.HtmlScriptElement" { | |||||||
|     try testing.htmlRunner("html/script/inline_defer.html"); |     try testing.htmlRunner("html/script/inline_defer.html"); | ||||||
| } | } | ||||||
|  |  | ||||||
| test "Browser: HTML.HtmlSlotElement" { | test "Browser: HTML.HTMLSlotElement" { | ||||||
|     try testing.htmlRunner("html/slot.html"); |     try testing.htmlRunner("html/html_slot_element.html"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const Check = struct { | ||||||
|  |     input: []const u8, | ||||||
|  |     expected: ?[]const u8 = null, // Needed when input != expected | ||||||
|  | }; | ||||||
|  | const bool_valids = [_]Check{ | ||||||
|  |     .{ .input = "true" }, | ||||||
|  |     .{ .input = "''", .expected = "false" }, | ||||||
|  |     .{ .input = "13.5", .expected = "true" }, | ||||||
|  | }; | ||||||
|  | const str_valids = [_]Check{ | ||||||
|  |     .{ .input = "'foo'", .expected = "foo" }, | ||||||
|  |     .{ .input = "5", .expected = "5" }, | ||||||
|  |     .{ .input = "''", .expected = "" }, | ||||||
|  |     .{ .input = "document", .expected = "[object HTMLDocument]" }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // .{ "elem.type = '5'", "5" }, | ||||||
|  | // .{ "elem.type", "text" }, | ||||||
|  | fn testProperty( | ||||||
|  |     arena: std.mem.Allocator, | ||||||
|  |     runner: *testing.JsRunner, | ||||||
|  |     elem_dot_prop: []const u8, | ||||||
|  |     always: ?[]const u8, // Ignores checks' expected if set | ||||||
|  |     checks: []const Check, | ||||||
|  | ) !void { | ||||||
|  |     for (checks) |check| { | ||||||
|  |         try runner.testCases(&.{ | ||||||
|  |             .{ try std.mem.concat(arena, u8, &.{ elem_dot_prop, " = ", check.input }), null }, | ||||||
|  |             .{ elem_dot_prop, always orelse check.expected orelse check.input }, | ||||||
|  |         }, .{}); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
							
								
								
									
										177
									
								
								src/browser/streams/ReadableStream.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								src/browser/streams/ReadableStream.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,177 @@ | |||||||
|  | // 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 = page.main_context.createPersistentPromiseResolver(); | ||||||
|  |     const closed_resolver = page.main_context.createPersistentPromiseResolver(); | ||||||
|  |  | ||||||
|  |     const stream = try page.arena.create(ReadableStream); | ||||||
|  |     stream.* = ReadableStream{ .cancel_resolver = cancel_resolver, .closed_resolver = closed_resolver, .strategy = strategy }; | ||||||
|  |  | ||||||
|  |     const controller = ReadableStreamDefaultController{ .stream = stream }; | ||||||
|  |  | ||||||
|  |     // call start | ||||||
|  |     if (underlying) |src| { | ||||||
|  |         if (src.start) |start| { | ||||||
|  |             try start.call(void, .{controller}); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (src.cancel) |cancel| { | ||||||
|  |             stream.cancel_fn = cancel; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (src.pull) |pull| { | ||||||
|  |             stream.pull_fn = pull; | ||||||
|  |             try stream.pullIf(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return stream; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn destructor(self: *ReadableStream) void { | ||||||
|  |     self.cancel_resolver.deinit(); | ||||||
|  |     self.closed_resolver.deinit(); | ||||||
|  |  | ||||||
|  |     if (self.reader_resolver) |*rr| { | ||||||
|  |         rr.deinit(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn get_locked(self: *const ReadableStream) bool { | ||||||
|  |     return self.locked; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn _cancel(self: *ReadableStream, reason: ?[]const u8, page: *Page) !Env.Promise { | ||||||
|  |     if (self.locked) { | ||||||
|  |         return error.TypeError; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     self.state = .{ .cancelled = if (reason) |r| try page.arena.dupe(u8, r) else null }; | ||||||
|  |  | ||||||
|  |     // Call cancel callback. | ||||||
|  |     if (self.cancel_fn) |cancel| { | ||||||
|  |         if (reason) |r| { | ||||||
|  |             try cancel.call(void, .{r}); | ||||||
|  |         } else { | ||||||
|  |             try cancel.call(void, .{}); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     try self.cancel_resolver.resolve({}); | ||||||
|  |     return self.cancel_resolver.promise(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn pullIf(self: *ReadableStream) !void { | ||||||
|  |     if (self.pull_fn) |pull_fn| { | ||||||
|  |         // Must be under the high water mark AND readable. | ||||||
|  |         if ((self.queue.items.len < self.strategy.high_water_mark) and self.state == .readable) { | ||||||
|  |             const controller = ReadableStreamDefaultController{ .stream = self }; | ||||||
|  |             try pull_fn.call(void, .{controller}); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const GetReaderOptions = struct { | ||||||
|  |     // Mode must equal 'byob' or be undefined. RangeError otherwise. | ||||||
|  |     mode: ?[]const u8 = null, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | pub fn _getReader(self: *ReadableStream, _options: ?GetReaderOptions) !ReadableStreamDefaultReader { | ||||||
|  |     if (self.locked) { | ||||||
|  |         return error.TypeError; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // TODO: Determine if we need the ReadableStreamBYOBReader | ||||||
|  |     const options = _options orelse GetReaderOptions{}; | ||||||
|  |     _ = options; | ||||||
|  |  | ||||||
|  |     return ReadableStreamDefaultReader.constructor(self); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TODO: pipeThrough (requires TransformStream) | ||||||
|  |  | ||||||
|  | // TODO: pipeTo (requires WritableStream) | ||||||
|  |  | ||||||
|  | // TODO: tee | ||||||
|  |  | ||||||
|  | const testing = @import("../../testing.zig"); | ||||||
|  | test "streams: ReadableStream" { | ||||||
|  |     try testing.htmlRunner("streams/readable_stream.html"); | ||||||
|  | } | ||||||
							
								
								
									
										82
									
								
								src/browser/streams/ReadableStreamDefaultController.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/browser/streams/ReadableStreamDefaultController.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | |||||||
|  | // Copyright (C) 2023-2024  Lightpanda (Selecy SAS) | ||||||
|  | // | ||||||
|  | // Francis Bouvier <francis@lightpanda.io> | ||||||
|  | // Pierre Tachoire <pierre@lightpanda.io> | ||||||
|  | // | ||||||
|  | // This program is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Affero General Public License as | ||||||
|  | // published by the Free Software Foundation, either version 3 of the | ||||||
|  | // License, or (at your option) any later version. | ||||||
|  | // | ||||||
|  | // This program is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | // GNU Affero General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Affero General Public License | ||||||
|  | // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | const std = @import("std"); | ||||||
|  | const log = @import("../../log.zig"); | ||||||
|  |  | ||||||
|  | const Page = @import("../page.zig").Page; | ||||||
|  | const Env = @import("../env.zig").Env; | ||||||
|  |  | ||||||
|  | const ReadableStream = @import("./ReadableStream.zig"); | ||||||
|  | const ReadableStreamReadResult = @import("./ReadableStream.zig").ReadableStreamReadResult; | ||||||
|  |  | ||||||
|  | const ReadableStreamDefaultController = @This(); | ||||||
|  |  | ||||||
|  | stream: *ReadableStream, | ||||||
|  |  | ||||||
|  | pub fn get_desiredSize(self: *const ReadableStreamDefaultController) i32 { | ||||||
|  |     // TODO: This may need tuning at some point if it becomes a performance issue. | ||||||
|  |     return @intCast(self.stream.queue.capacity - self.stream.queue.items.len); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn _close(self: *ReadableStreamDefaultController, _reason: ?[]const u8, page: *Page) !void { | ||||||
|  |     const reason = if (_reason) |reason| try page.arena.dupe(u8, reason) else null; | ||||||
|  |     self.stream.state = .{ .closed = reason }; | ||||||
|  |  | ||||||
|  |     // Resolve the Reader Promise | ||||||
|  |     if (self.stream.reader_resolver) |*rr| { | ||||||
|  |         defer rr.deinit(); | ||||||
|  |         try rr.resolve(ReadableStreamReadResult{ .value = .empty, .done = true }); | ||||||
|  |         self.stream.reader_resolver = null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Resolve the Closed promise. | ||||||
|  |     try self.stream.closed_resolver.resolve({}); | ||||||
|  |  | ||||||
|  |     // close just sets as closed meaning it wont READ any more but anything in the queue is fine to read. | ||||||
|  |     // to discard, must use cancel. | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn _enqueue(self: *ReadableStreamDefaultController, chunk: []const u8, page: *Page) !void { | ||||||
|  |     const stream = self.stream; | ||||||
|  |  | ||||||
|  |     if (stream.state != .readable) { | ||||||
|  |         return error.TypeError; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const duped_chunk = try page.arena.dupe(u8, chunk); | ||||||
|  |  | ||||||
|  |     if (self.stream.reader_resolver) |*rr| { | ||||||
|  |         defer rr.deinit(); | ||||||
|  |         try rr.resolve(ReadableStreamReadResult{ .value = .{ .data = duped_chunk }, .done = false }); | ||||||
|  |         self.stream.reader_resolver = null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     try self.stream.queue.append(page.arena, duped_chunk); | ||||||
|  |     try self.stream.pullIf(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn _error(self: *ReadableStreamDefaultController, err: Env.JsObject) !void { | ||||||
|  |     self.stream.state = .{ .errored = err }; | ||||||
|  |  | ||||||
|  |     if (self.stream.reader_resolver) |*rr| { | ||||||
|  |         defer rr.deinit(); | ||||||
|  |         try rr.reject(err); | ||||||
|  |         self.stream.reader_resolver = null; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										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 = page.main_context.createPersistentPromiseResolver(); | ||||||
|  |                     self.stream.reader_resolver = persistent_resolver; | ||||||
|  |                     return persistent_resolver.promise(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         .closed => |_| { | ||||||
|  |             const resolver = page.main_context.createPromiseResolver(); | ||||||
|  |  | ||||||
|  |             if (stream.queue.items.len > 0) { | ||||||
|  |                 const data = self.stream.queue.orderedRemove(0); | ||||||
|  |                 try resolver.resolve(ReadableStreamReadResult{ .value = .{ .data = data }, .done = false }); | ||||||
|  |             } else { | ||||||
|  |                 try resolver.resolve(ReadableStreamReadResult{ .value = .empty, .done = true }); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return resolver.promise(); | ||||||
|  |         }, | ||||||
|  |         .cancelled => |_| { | ||||||
|  |             const resolver = page.main_context.createPromiseResolver(); | ||||||
|  |             try resolver.resolve(ReadableStreamReadResult{ .value = .empty, .done = true }); | ||||||
|  |             return resolver.promise(); | ||||||
|  |         }, | ||||||
|  |         .errored => |err| { | ||||||
|  |             const resolver = page.main_context.createPromiseResolver(); | ||||||
|  |             try resolver.reject(err); | ||||||
|  |             return resolver.promise(); | ||||||
|  |         }, | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn _releaseLock(self: *const ReadableStreamDefaultReader) !void { | ||||||
|  |     self.stream.locked = false; | ||||||
|  |  | ||||||
|  |     if (self.stream.reader_resolver) |rr| { | ||||||
|  |         try rr.reject("TypeError"); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										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", | ||||||
|   | |||||||
| @@ -649,6 +649,7 @@ pub const Request = struct { | |||||||
|         document, |         document, | ||||||
|         xhr, |         xhr, | ||||||
|         script, |         script, | ||||||
|  |         fetch, | ||||||
|     }; |     }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -339,13 +339,13 @@ pub const Opts = struct { | |||||||
|     proxy_bearer_token: ?[:0]const u8 = null, |     proxy_bearer_token: ?[:0]const u8 = null, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| pub const Method = enum { | pub const Method = enum(u8) { | ||||||
|     GET, |     GET = 0, | ||||||
|     PUT, |     PUT = 1, | ||||||
|     POST, |     POST = 2, | ||||||
|     DELETE, |     DELETE = 3, | ||||||
|     HEAD, |     HEAD = 4, | ||||||
|     OPTIONS, |     OPTIONS = 5, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| // TODO: on BSD / Linux, we could just read the PEM file directly. | // TODO: on BSD / Linux, we could just read the PEM file directly. | ||||||
|   | |||||||
| @@ -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, | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1130,6 +1130,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 +1261,13 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { | |||||||
|                 }; |                 }; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             pub fn createPersistentPromiseResolver(self: *JsContext) PersistentPromiseResolver { | ||||||
|  |                 return .{ | ||||||
|  |                     .js_context = self, | ||||||
|  |                     .resolver = v8.Persistent(v8.PromiseResolver).init(self.isolate, v8.PromiseResolver.init(self.v8_context)), | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|  |  | ||||||
|             // 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 +1430,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 +2225,58 @@ 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 setWeak(self: *PersistentPromiseResolver) void { | ||||||
|  |                 self.resolver.setWeak(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             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> | ||||||
							
								
								
									
										66
									
								
								src/tests/html/html_slot_element.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/tests/html/html_slot_element.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | |||||||
|  | <script src="../testing.js"></script> | ||||||
|  | <script> | ||||||
|  |   class LightPanda extends HTMLElement { | ||||||
|  |     constructor() { | ||||||
|  |       super(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     connectedCallback() { | ||||||
|  |       const shadow = this.attachShadow({ mode: "open" }); | ||||||
|  |  | ||||||
|  |       const slot1 = document.createElement('slot'); | ||||||
|  |       slot1.name = 'slot-1'; | ||||||
|  |       shadow.appendChild(slot1); | ||||||
|  |  | ||||||
|  |       switch (this.getAttribute('mode')) { | ||||||
|  |         case '1': | ||||||
|  |           slot1.innerHTML = 'hello'; | ||||||
|  |           break; | ||||||
|  |         case '2': | ||||||
|  |           const slot2 = document.createElement('slot'); | ||||||
|  |           shadow.appendChild(slot2); | ||||||
|  |           break; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   window.customElements.define("lp-test", LightPanda); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <lp-test id=lp1 mode=1></lp-test> | ||||||
|  | <lp-test id=lp2 mode=0></lp-test> | ||||||
|  | <lp-test id=lp3 mode=0>default</lp-test> | ||||||
|  | <lp-test id=lp4 mode=1><p slot=other>default</p></lp-test> | ||||||
|  | <lp-test id=lp5 mode=1><p slot=slot-1>default</p> xx <b slot=slot-1>other</b></lp-test> | ||||||
|  | <lp-test id=lp6 mode=2>More <p slot=slot-1>default2</p> <span>!!</span></lp-test> | ||||||
|  |  | ||||||
|  | <script id=HTMLSlotElement> | ||||||
|  |   function assertNodes(expected, actual) { | ||||||
|  |     actual = actual.map((n) => n.id || n.textContent) | ||||||
|  |     testing.expectEqual(expected, actual); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   for (let idx of [1, 2, 3, 4]) { | ||||||
|  |     const lp = $(`#lp${idx}`); | ||||||
|  |     const slot = lp.shadowRoot.querySelector('slot'); | ||||||
|  |  | ||||||
|  |     assertNodes([], slot.assignedNodes()); | ||||||
|  |     assertNodes([], slot.assignedNodes({})); | ||||||
|  |     assertNodes([], slot.assignedNodes({flatten: false})); | ||||||
|  |     if (lp.getAttribute('mode') === '1') { | ||||||
|  |       assertNodes(['hello'], slot.assignedNodes({flatten: true})); | ||||||
|  |     } else { | ||||||
|  |       assertNodes([], slot.assignedNodes({flatten: true})); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const lp5 = $('#lp5'); | ||||||
|  |   const s5 = lp5.shadowRoot.querySelector('slot'); | ||||||
|  |   assertNodes(['default', 'other'], s5.assignedNodes()); | ||||||
|  |  | ||||||
|  |   const lp6 = $('#lp6'); | ||||||
|  |   const s6 = lp6.shadowRoot.querySelectorAll('slot'); | ||||||
|  |   assertNodes(['default2'], s6[0].assignedNodes({})); | ||||||
|  |   assertNodes(['default2'], s6[0].assignedNodes({flatten: true})); | ||||||
|  |   assertNodes(['More ', ' ', '!!'], s6[1].assignedNodes({})); | ||||||
|  |   assertNodes(['More ', ' ', '!!'], s6[1].assignedNodes({flatten: true})); | ||||||
|  | </script> | ||||||
							
								
								
									
										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