mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-24 05:33:16 +00:00
Merge branch 'main' into semantic-tree
This commit is contained in:
5
.github/workflows/wpt.yml
vendored
5
.github/workflows/wpt.yml
vendored
@@ -5,6 +5,7 @@ env:
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
||||
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
||||
AWS_CF_DISTRIBUTION: ${{ vars.AWS_CF_DISTRIBUTION }}
|
||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||
|
||||
on:
|
||||
@@ -73,7 +74,7 @@ jobs:
|
||||
|
||||
# use a self host runner.
|
||||
runs-on: lpd-bench-hetzner
|
||||
timeout-minutes: 120
|
||||
timeout-minutes: 180
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -107,7 +108,7 @@ jobs:
|
||||
run: |
|
||||
./wpt serve 2> /dev/null & echo $! > WPT.pid
|
||||
sleep 10s
|
||||
./wptrunner -lpd-path ./lightpanda -json -concurrency 3 > wpt.json
|
||||
./wptrunner -lpd-path ./lightpanda -json -concurrency 6 > wpt.json
|
||||
kill `cat WPT.pid`
|
||||
|
||||
- name: write commit
|
||||
|
||||
15
src/App.zig
15
src/App.zig
@@ -47,10 +47,17 @@ pub fn init(allocator: Allocator, config: *const Config) !*App {
|
||||
const app = try allocator.create(App);
|
||||
errdefer allocator.destroy(app);
|
||||
|
||||
app.config = config;
|
||||
app.allocator = allocator;
|
||||
|
||||
app.robots = RobotStore.init(allocator);
|
||||
app.* = .{
|
||||
.config = config,
|
||||
.allocator = allocator,
|
||||
.robots = RobotStore.init(allocator),
|
||||
.http = undefined,
|
||||
.platform = undefined,
|
||||
.snapshot = undefined,
|
||||
.app_dir_path = undefined,
|
||||
.telemetry = undefined,
|
||||
.arena_pool = undefined,
|
||||
};
|
||||
|
||||
app.http = try Http.init(allocator, &app.robots, config);
|
||||
errdefer app.http.deinit();
|
||||
|
||||
@@ -69,6 +69,7 @@ const ArenaPool = App.ArenaPool;
|
||||
const timestamp = @import("../datetime.zig").timestamp;
|
||||
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
|
||||
|
||||
const IFrame = Element.Html.IFrame;
|
||||
const WebApiURL = @import("webapi/URL.zig");
|
||||
const GlobalEventHandlersLookup = @import("webapi/global_event_handlers.zig").Lookup;
|
||||
|
||||
@@ -223,7 +224,7 @@ _arena_pool_leak_track: (if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, struct {
|
||||
parent: ?*Page,
|
||||
window: *Window,
|
||||
document: *Document,
|
||||
iframe: ?*Element.Html.IFrame = null,
|
||||
iframe: ?*IFrame = null,
|
||||
frames: std.ArrayList(*Page) = .{},
|
||||
frames_sorted: bool = true,
|
||||
|
||||
@@ -323,9 +324,9 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Page) void {
|
||||
pub fn deinit(self: *Page, abort_http: bool) void {
|
||||
for (self.frames.items) |frame| {
|
||||
frame.deinit();
|
||||
frame.deinit(abort_http);
|
||||
}
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
@@ -346,10 +347,16 @@ pub fn deinit(self: *Page) void {
|
||||
session.browser.env.destroyContext(self.js);
|
||||
|
||||
self._script_manager.shutdown = true;
|
||||
|
||||
if (self.parent == null) {
|
||||
// only the root frame needs to abort this. It's more efficient this way
|
||||
session.browser.http_client.abort();
|
||||
} else if (abort_http) {
|
||||
// a small optimization, it's faster to abort _everything_ on the root
|
||||
// page, so we prefer that. But if it's just the frame that's going
|
||||
// away (a frame navigation) then we'll abort the frame-related requests
|
||||
session.browser.http_client.abortFrame(self._frame_id);
|
||||
}
|
||||
|
||||
self._script_manager.deinit();
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
@@ -357,6 +364,9 @@ pub fn deinit(self: *Page) void {
|
||||
while (it.next()) |value_ptr| {
|
||||
if (value_ptr.count > 0) {
|
||||
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner, .type = self._type, .url = self.url });
|
||||
if (comptime builtin.is_test) {
|
||||
@panic("ArenaPool Leak");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,6 +439,9 @@ pub fn releaseArena(self: *Page, allocator: Allocator) void {
|
||||
const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?;
|
||||
if (found.count != 1) {
|
||||
log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count, .type = self._type, .url = self.url });
|
||||
if (comptime builtin.is_test) {
|
||||
@panic("ArenaPool Double Free");
|
||||
}
|
||||
return;
|
||||
}
|
||||
found.count = 0;
|
||||
@@ -459,16 +472,15 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
||||
// if the url is about:blank, we load an empty HTML document in the
|
||||
// page and dispatch the events.
|
||||
if (std.mem.eql(u8, "about:blank", request_url)) {
|
||||
self.url = "about:blank";
|
||||
// Assume we parsed the document.
|
||||
// It's important to force a reset during the following navigation.
|
||||
self._parse_state = .complete;
|
||||
|
||||
{
|
||||
const parse_arena = try self.getArena(.{ .debug = "about:blank parse" });
|
||||
defer self.releaseArena(parse_arena);
|
||||
var parser = Parser.init(parse_arena, self.document.asNode(), self);
|
||||
parser.parse("<html><head></head><body></body></html>");
|
||||
}
|
||||
self.document.injectBlank(self) catch |err| {
|
||||
log.err(.browser, "inject blank", .{ .err = err });
|
||||
return error.InjectBlankFailed;
|
||||
};
|
||||
self.documentIsComplete();
|
||||
|
||||
session.notification.dispatch(.page_navigate, &.{
|
||||
@@ -559,57 +571,89 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
||||
};
|
||||
}
|
||||
|
||||
// We cannot navigate immediately as navigating will delete the DOM tree,
|
||||
// which holds this event's node.
|
||||
// As such we schedule the function to be called as soon as possible.
|
||||
pub fn scheduleNavigation(self: *Page, request_url: []const u8, opts: NavigateOpts, priority: NavigationPriority) !void {
|
||||
if (self.canScheduleNavigation(priority) == false) {
|
||||
// Navigation can happen in many places, such as executing a <script> tag or
|
||||
// a JavaScript callback, a CDP command, etc...It's rarely safe to do immediately
|
||||
// as the caller almost certainly does'nt expect the page to go away during the
|
||||
// call. So, we schedule the navigation for the next tick.
|
||||
pub fn scheduleNavigation(self: *Page, request_url: []const u8, opts: NavigateOpts, nt: Navigation) !void {
|
||||
if (self.canScheduleNavigation(std.meta.activeTag(nt)) == false) {
|
||||
return;
|
||||
}
|
||||
const arena = try self.arena_pool.acquire();
|
||||
errdefer self.arena_pool.release(arena);
|
||||
return self.scheduleNavigationWithArena(arena, request_url, opts, priority);
|
||||
return self.scheduleNavigationWithArena(arena, request_url, opts, nt);
|
||||
}
|
||||
|
||||
fn scheduleNavigationWithArena(self: *Page, arena: Allocator, request_url: []const u8, opts: NavigateOpts, priority: NavigationPriority) !void {
|
||||
const resolved_url = try URL.resolve(
|
||||
arena,
|
||||
self.base(),
|
||||
request_url,
|
||||
.{ .always_dupe = true, .encode = true },
|
||||
);
|
||||
// Don't name the first parameter "self", because the target of this navigation
|
||||
// might change inside the function. So the code should be explicit about the
|
||||
// page that it's acting on.
|
||||
fn scheduleNavigationWithArena(originator: *Page, arena: Allocator, request_url: []const u8, opts: NavigateOpts, nt: Navigation) !void {
|
||||
const resolved_url, const is_about_blank = blk: {
|
||||
if (std.mem.eql(u8, request_url, "about:blank")) {
|
||||
// navigate will handle this special case
|
||||
break :blk .{ "about:blank", true };
|
||||
}
|
||||
const u = try URL.resolve(
|
||||
arena,
|
||||
originator.base(),
|
||||
request_url,
|
||||
.{ .always_dupe = true, .encode = true },
|
||||
);
|
||||
break :blk .{ u, false };
|
||||
};
|
||||
|
||||
const session = self._session;
|
||||
if (!opts.force and URL.eqlDocument(self.url, resolved_url)) {
|
||||
self.arena_pool.release(arena);
|
||||
const target = switch (nt) {
|
||||
.script => |p| p orelse originator,
|
||||
.iframe => |iframe| iframe._window.?._page, // only an frame with existing content (i.e. a window) can be navigated
|
||||
.anchor, .form => |node| blk: {
|
||||
const doc = node.ownerDocument(originator) orelse break :blk originator;
|
||||
break :blk doc._page orelse originator;
|
||||
},
|
||||
};
|
||||
|
||||
self.url = try self.arena.dupeZ(u8, resolved_url);
|
||||
self.window._location = try Location.init(self.url, self);
|
||||
self.document._location = self.window._location;
|
||||
return session.navigation.updateEntries(self.url, opts.kind, self, true);
|
||||
const session = target._session;
|
||||
if (!opts.force and URL.eqlDocument(target.url, resolved_url)) {
|
||||
target.url = try target.arena.dupeZ(u8, resolved_url);
|
||||
target.window._location = try Location.init(target.url, target);
|
||||
target.document._location = target.window._location;
|
||||
if (target.parent == null) {
|
||||
try session.navigation.updateEntries(target.url, opts.kind, target, true);
|
||||
}
|
||||
// doin't defer this, the caller, the caller is responsible for freeing
|
||||
// it on error
|
||||
target.arena_pool.release(arena);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(.browser, "schedule navigation", .{
|
||||
.url = resolved_url,
|
||||
.reason = opts.reason,
|
||||
.target = resolved_url,
|
||||
.type = self._type,
|
||||
.type = target._type,
|
||||
});
|
||||
|
||||
session.browser.http_client.abort();
|
||||
// This is a micro-optimization. Terminate any inflight request as early
|
||||
// as we can. This will be more propery shutdown when we process the
|
||||
// scheduled navigation.
|
||||
if (target.parent == null) {
|
||||
session.browser.http_client.abort();
|
||||
} else {
|
||||
// This doesn't terminate any inflight requests for nested frames, but
|
||||
// again, this is just an optimization. We'll correctly shut down all
|
||||
// nested inflight requests when we process the navigation.
|
||||
session.browser.http_client.abortFrame(target._frame_id);
|
||||
}
|
||||
|
||||
const qn = try arena.create(QueuedNavigation);
|
||||
qn.* = .{
|
||||
.opts = opts,
|
||||
.arena = arena,
|
||||
.url = resolved_url,
|
||||
.priority = priority,
|
||||
.is_about_blank = is_about_blank,
|
||||
.navigation_type = std.meta.activeTag(nt),
|
||||
};
|
||||
|
||||
if (self._queued_navigation) |existing| {
|
||||
self.arena_pool.release(existing.arena);
|
||||
}
|
||||
self._queued_navigation = qn;
|
||||
target._queued_navigation = qn;
|
||||
return session.scheduleNavigation(target);
|
||||
}
|
||||
|
||||
// A script can have multiple competing navigation events, say it starts off
|
||||
@@ -617,23 +661,31 @@ fn scheduleNavigationWithArena(self: *Page, arena: Allocator, request_url: []con
|
||||
// You might think that we just stop at the first one, but that doesn't seem
|
||||
// to be what browsers do, and it isn't particularly well supported by v8 (i.e.
|
||||
// halting execution mid-script).
|
||||
// From what I can tell, there are 3 "levels" of priority, in order:
|
||||
// From what I can tell, there are 4 "levels" of priority, in order:
|
||||
// 1 - form submission
|
||||
// 2 - JavaScript apis (e.g. top.location)
|
||||
// 3 - anchor clicks
|
||||
// 4 - iframe.src =
|
||||
// Within, each category, it's last-one-wins.
|
||||
fn canScheduleNavigation(self: *Page, priority: NavigationPriority) bool {
|
||||
const existing = self._queued_navigation orelse return true;
|
||||
fn canScheduleNavigation(self: *Page, new_target_type: NavigationType) bool {
|
||||
if (self.parent) |parent| {
|
||||
if (parent.isGoingAway()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (existing.priority == priority) {
|
||||
const existing_target_type = (self._queued_navigation orelse return true).navigation_type;
|
||||
|
||||
if (existing_target_type == new_target_type) {
|
||||
// same reason, than this latest one wins
|
||||
return true;
|
||||
}
|
||||
|
||||
return switch (existing.priority) {
|
||||
.anchor => true, // everything is higher priority than an anchor
|
||||
return switch (existing_target_type) {
|
||||
.iframe => true, // everything is higher priority than iframe.src = "x"
|
||||
.anchor => new_target_type != .iframe, // an anchor is only higher priority than an iframe
|
||||
.form => false, // nothing is higher priority than a form
|
||||
.script => priority == .form, // a form is higher priority than a script
|
||||
.script => new_target_type == .form, // a form is higher priority than a script
|
||||
};
|
||||
}
|
||||
|
||||
@@ -665,7 +717,7 @@ pub fn scriptsCompletedLoading(self: *Page) void {
|
||||
self.pendingLoadCompleted();
|
||||
}
|
||||
|
||||
pub fn iframeCompletedLoading(self: *Page, iframe: *Element.Html.IFrame) void {
|
||||
pub fn iframeCompletedLoading(self: *Page, iframe: *IFrame) void {
|
||||
blk: {
|
||||
var ls: JS.Local.Scope = undefined;
|
||||
self.js.localScope(&ls);
|
||||
@@ -758,6 +810,8 @@ fn _documentIsComplete(self: *Page) !void {
|
||||
}
|
||||
|
||||
fn notifyParentLoadComplete(self: *Page) void {
|
||||
const parent = self.parent orelse return;
|
||||
|
||||
if (self._parent_notified == true) {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(false);
|
||||
@@ -767,9 +821,7 @@ fn notifyParentLoadComplete(self: *Page) void {
|
||||
}
|
||||
|
||||
self._parent_notified = true;
|
||||
if (self.parent) |p| {
|
||||
p.iframeCompletedLoading(self.iframe.?);
|
||||
}
|
||||
parent.iframeCompletedLoading(self.iframe.?);
|
||||
}
|
||||
|
||||
fn pageHeaderDoneCallback(transfer: *Http.Transfer) !bool {
|
||||
@@ -949,7 +1001,11 @@ fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
}
|
||||
|
||||
pub fn isGoingAway(self: *const Page) bool {
|
||||
return self._queued_navigation != null;
|
||||
if (self._queued_navigation != null) {
|
||||
return true;
|
||||
}
|
||||
const parent = self.parent orelse return false;
|
||||
return parent.isGoingAway();
|
||||
}
|
||||
|
||||
pub fn scriptAddedCallback(self: *Page, comptime from_parser: bool, script: *Element.Html.Script) !void {
|
||||
@@ -968,7 +1024,7 @@ pub fn scriptAddedCallback(self: *Page, comptime from_parser: bool, script: *Ele
|
||||
};
|
||||
}
|
||||
|
||||
pub fn iframeAddedCallback(self: *Page, iframe: *Element.Html.IFrame) !void {
|
||||
pub fn iframeAddedCallback(self: *Page, iframe: *IFrame) !void {
|
||||
if (self.isGoingAway()) {
|
||||
// if we're planning on navigating to another page, don't load this iframe
|
||||
return;
|
||||
@@ -977,34 +1033,43 @@ pub fn iframeAddedCallback(self: *Page, iframe: *Element.Html.IFrame) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
const src = iframe.asElement().getAttributeSafe(comptime .wrap("src")) orelse return;
|
||||
var src = iframe.asElement().getAttributeSafe(comptime .wrap("src")) orelse "";
|
||||
if (src.len == 0) {
|
||||
return;
|
||||
src = "about:blank";
|
||||
}
|
||||
|
||||
if (iframe._window != null) {
|
||||
// This frame is being re-navigated. We need to do this through a
|
||||
// scheduleNavigation phase. We can't navigate immediately here, for
|
||||
// the same reason that a "root" page can't immediately navigate:
|
||||
// we could be in the middle of a JS callback or something else that
|
||||
// doesn't exit the page to just suddenly go away.
|
||||
return self.scheduleNavigation(src, .{
|
||||
.reason = .script,
|
||||
.kind = .{ .push = null },
|
||||
}, .{ .iframe = iframe });
|
||||
}
|
||||
|
||||
iframe._executed = true;
|
||||
const session = self._session;
|
||||
|
||||
// A frame can be re-navigated by setting the src.
|
||||
const existing_window = iframe._content_window;
|
||||
|
||||
const page_frame = try self.arena.create(Page);
|
||||
const frame_id = blk: {
|
||||
if (existing_window) |w| {
|
||||
const existing_frame_id = w._page._frame_id;
|
||||
session.browser.http_client.abortFrame(existing_frame_id);
|
||||
break :blk existing_frame_id;
|
||||
}
|
||||
break :blk session.nextFrameId();
|
||||
};
|
||||
const frame_id = session.nextFrameId();
|
||||
|
||||
try Page.init(page_frame, frame_id, session, self);
|
||||
errdefer page_frame.deinit();
|
||||
errdefer page_frame.deinit(true);
|
||||
|
||||
self._pending_loads += 1;
|
||||
page_frame.iframe = iframe;
|
||||
iframe._content_window = page_frame.window;
|
||||
errdefer iframe._content_window = null;
|
||||
iframe._window = page_frame.window;
|
||||
errdefer iframe._window = null;
|
||||
|
||||
// on first load, dispatch frame_created evnet
|
||||
self._session.notification.dispatch(.page_frame_created, &.{
|
||||
.frame_id = frame_id,
|
||||
.parent_id = self._frame_id,
|
||||
.timestamp = timestamp(.monotonic),
|
||||
});
|
||||
|
||||
const url = blk: {
|
||||
if (std.mem.eql(u8, src, "about:blank")) {
|
||||
@@ -1018,42 +1083,14 @@ pub fn iframeAddedCallback(self: *Page, iframe: *Element.Html.IFrame) !void {
|
||||
);
|
||||
};
|
||||
|
||||
if (existing_window == null) {
|
||||
// on first load, dispatch frame_created evnet
|
||||
self._session.notification.dispatch(.page_frame_created, &.{
|
||||
.frame_id = frame_id,
|
||||
.parent_id = self._frame_id,
|
||||
.timestamp = timestamp(.monotonic),
|
||||
});
|
||||
}
|
||||
|
||||
page_frame.navigate(url, .{ .reason = .initialFrameNavigation }) catch |err| {
|
||||
log.warn(.page, "iframe navigate failure", .{ .url = url, .err = err });
|
||||
self._pending_loads -= 1;
|
||||
iframe._content_window = null;
|
||||
page_frame.deinit();
|
||||
iframe._window = null;
|
||||
page_frame.deinit(true);
|
||||
return error.IFrameLoadError;
|
||||
};
|
||||
|
||||
if (existing_window) |w| {
|
||||
const existing_page = w._page;
|
||||
if (existing_page._parent_notified == false) {
|
||||
self._pending_loads -= 1;
|
||||
}
|
||||
|
||||
for (self.frames.items, 0..) |p, i| {
|
||||
if (p == existing_page) {
|
||||
self.frames.items[i] = page_frame;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
lp.assert(false, "Existing frame not found", .{ .len = self.frames.items.len });
|
||||
}
|
||||
|
||||
existing_page.deinit();
|
||||
return;
|
||||
}
|
||||
|
||||
// window[N] is based on document order. For now we'll just append the frame
|
||||
// at the end of our list and set frames_sorted == false. window.getFrame
|
||||
// will check this flag to decide if it needs to sort the frames or not.
|
||||
@@ -1969,7 +2006,7 @@ pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const
|
||||
.{ ._proto = undefined },
|
||||
),
|
||||
asUint("iframe") => return self.createHtmlElementT(
|
||||
Element.Html.IFrame,
|
||||
IFrame,
|
||||
namespace,
|
||||
attribute_iterator,
|
||||
.{ ._proto = undefined },
|
||||
@@ -2880,12 +2917,7 @@ fn nodeIsReady(self: *Page, comptime from_parser: bool, node: *Node) !void {
|
||||
log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "script", .type = self._type, .url = self.url });
|
||||
return err;
|
||||
};
|
||||
} else if (node.is(Element.Html.IFrame)) |iframe| {
|
||||
if ((comptime from_parser == false) and iframe._src.len == 0) {
|
||||
// iframe was added via JavaScript, but without a src
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (node.is(IFrame)) |iframe| {
|
||||
self.iframeAddedCallback(iframe) catch |err| {
|
||||
log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "iframe", .type = self._type, .url = self.url });
|
||||
return err;
|
||||
@@ -3026,17 +3058,26 @@ pub const NavigatedOpts = struct {
|
||||
method: Http.Method = .GET,
|
||||
};
|
||||
|
||||
const NavigationPriority = enum {
|
||||
const NavigationType = enum {
|
||||
form,
|
||||
script,
|
||||
anchor,
|
||||
iframe,
|
||||
};
|
||||
|
||||
const Navigation = union(NavigationType) {
|
||||
form: *Node,
|
||||
script: ?*Page,
|
||||
anchor: *Node,
|
||||
iframe: *IFrame,
|
||||
};
|
||||
|
||||
pub const QueuedNavigation = struct {
|
||||
arena: Allocator,
|
||||
url: [:0]const u8,
|
||||
opts: NavigateOpts,
|
||||
priority: NavigationPriority,
|
||||
is_about_blank: bool,
|
||||
navigation_type: NavigationType,
|
||||
};
|
||||
|
||||
pub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void {
|
||||
@@ -3089,11 +3130,17 @@ pub fn handleClick(self: *Page, target: *Node) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: We need to support targets properly, but this is the most
|
||||
// common case: a click on an anchor navigates the page/frame that
|
||||
// anchor is in.
|
||||
|
||||
// ownerDocument only returns null when `target` is a document, which
|
||||
// it is NOT in this case. Even for a detched node, it'll return self.document
|
||||
try element.focus(self);
|
||||
try self.scheduleNavigation(href, .{
|
||||
.reason = .script,
|
||||
.kind = .{ .push = null },
|
||||
}, .anchor);
|
||||
}, .{ .anchor = target });
|
||||
},
|
||||
.input => |input| {
|
||||
try element.focus(self);
|
||||
@@ -3114,7 +3161,11 @@ pub fn handleClick(self: *Page, target: *Node) !void {
|
||||
|
||||
pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
|
||||
const event = keyboard_event.asEvent();
|
||||
const element = self.window._document._active_element orelse return;
|
||||
const element = self.window._document._active_element orelse {
|
||||
keyboard_event.deinit(false, self);
|
||||
return;
|
||||
};
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.page, "page keydown", .{
|
||||
.url = self.url,
|
||||
@@ -3224,7 +3275,7 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
|
||||
} else {
|
||||
action = try URL.concatQueryString(arena, action, buf.written());
|
||||
}
|
||||
return self.scheduleNavigationWithArena(arena, action, opts, .form);
|
||||
return self.scheduleNavigationWithArena(arena, action, opts, .{ .form = form_element.asNode() });
|
||||
}
|
||||
|
||||
// insertText is a shortcut to insert text into the active element.
|
||||
|
||||
@@ -30,6 +30,7 @@ const History = @import("webapi/History.zig");
|
||||
const Page = @import("Page.zig");
|
||||
const Browser = @import("Browser.zig");
|
||||
const Notification = @import("../Notification.zig");
|
||||
const QueuedNavigation = Page.QueuedNavigation;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
@@ -43,6 +44,12 @@ const Session = @This();
|
||||
browser: *Browser,
|
||||
notification: *Notification,
|
||||
|
||||
queued_navigation: std.ArrayList(*Page),
|
||||
// Temporary buffer for about:blank navigations during processing.
|
||||
// We process async navigations first (safe from re-entrance), then sync
|
||||
// about:blank navigations (which may add to queued_navigation).
|
||||
queued_queued_navigation: std.ArrayList(*Page),
|
||||
|
||||
// Used to create our Inspector and in the BrowserContext.
|
||||
arena: Allocator,
|
||||
|
||||
@@ -70,6 +77,8 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi
|
||||
.navigation = .{ ._proto = undefined },
|
||||
.storage_shed = .{},
|
||||
.browser = browser,
|
||||
.queued_navigation = .{},
|
||||
.queued_queued_navigation = .{},
|
||||
.notification = notification,
|
||||
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
||||
};
|
||||
@@ -79,9 +88,9 @@ pub fn deinit(self: *Session) void {
|
||||
if (self.page != null) {
|
||||
self.removePage();
|
||||
}
|
||||
const browser = self.browser;
|
||||
|
||||
self.cookie_jar.deinit();
|
||||
|
||||
const browser = self.browser;
|
||||
self.storage_shed.deinit(browser.app.allocator);
|
||||
browser.arena_pool.release(self.arena);
|
||||
}
|
||||
@@ -113,7 +122,7 @@ pub fn removePage(self: *Session) void {
|
||||
self.notification.dispatch(.page_remove, .{});
|
||||
lp.assert(self.page != null, "Session.removePage - page is null", .{});
|
||||
|
||||
self.page.?.deinit();
|
||||
self.page.?.deinit(false);
|
||||
self.page = null;
|
||||
|
||||
self.navigation.onRemovePage();
|
||||
@@ -133,7 +142,7 @@ pub fn replacePage(self: *Session) !*Page {
|
||||
var current = self.page.?;
|
||||
const frame_id = current._frame_id;
|
||||
const parent = current.parent;
|
||||
current.deinit();
|
||||
current.deinit(false);
|
||||
|
||||
self.browser.env.memoryPressureNotification(.moderate);
|
||||
|
||||
@@ -174,10 +183,11 @@ pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
||||
|
||||
switch (wait_result) {
|
||||
.done => {
|
||||
if (page._queued_navigation == null) {
|
||||
if (self.queued_navigation.items.len == 0) {
|
||||
return .done;
|
||||
}
|
||||
page = self.processScheduledNavigation(page) catch return .done;
|
||||
self.processQueuedNavigation() catch return .done;
|
||||
page = &self.page.?; // might have changed
|
||||
},
|
||||
else => |result| return result,
|
||||
}
|
||||
@@ -229,7 +239,7 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
||||
}
|
||||
},
|
||||
.html, .complete => {
|
||||
if (page._queued_navigation != null) {
|
||||
if (self.queued_navigation.items.len != 0) {
|
||||
return .done;
|
||||
}
|
||||
|
||||
@@ -345,42 +355,134 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
||||
}
|
||||
}
|
||||
|
||||
fn processScheduledNavigation(self: *Session, current_page: *Page) !*Page {
|
||||
const browser = self.browser;
|
||||
pub fn scheduleNavigation(self: *Session, page: *Page) !void {
|
||||
const list = &self.queued_navigation;
|
||||
|
||||
const qn = current_page._queued_navigation.?;
|
||||
// take ownership of the page's queued navigation
|
||||
current_page._queued_navigation = null;
|
||||
// Check if page is already queued
|
||||
for (list.items) |existing| {
|
||||
if (existing == page) {
|
||||
// Already queued
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return list.append(self.arena, page);
|
||||
}
|
||||
|
||||
fn processQueuedNavigation(self: *Session) !void {
|
||||
const navigations = &self.queued_navigation;
|
||||
|
||||
if (self.page.?._queued_navigation != null) {
|
||||
// This is both an optimization and a simplification of sorts. If the
|
||||
// root page is navigating, then we don't need to process any other
|
||||
// navigation. Also, the navigation for the root page and for a frame
|
||||
// is different enough that have two distinct code blocks is, imo,
|
||||
// better. Yes, there will be duplication.
|
||||
navigations.clearRetainingCapacity();
|
||||
return self.processRootQueuedNavigation();
|
||||
}
|
||||
|
||||
const about_blank_queue = &self.queued_queued_navigation;
|
||||
defer about_blank_queue.clearRetainingCapacity();
|
||||
|
||||
// First pass: process async navigations (non-about:blank)
|
||||
// These cannot cause re-entrant navigation scheduling
|
||||
for (navigations.items) |page| {
|
||||
const qn = page._queued_navigation.?;
|
||||
|
||||
if (qn.is_about_blank) {
|
||||
// Defer about:blank to second pass
|
||||
try about_blank_queue.append(self.arena, page);
|
||||
continue;
|
||||
}
|
||||
|
||||
try self.processFrameNavigation(page, qn);
|
||||
}
|
||||
|
||||
// Clear the queue after first pass
|
||||
navigations.clearRetainingCapacity();
|
||||
|
||||
// Second pass: process synchronous navigations (about:blank)
|
||||
// These may trigger new navigations which go into queued_navigation
|
||||
for (about_blank_queue.items) |page| {
|
||||
const qn = page._queued_navigation.?;
|
||||
try self.processFrameNavigation(page, qn);
|
||||
}
|
||||
|
||||
// Safety: Remove any about:blank navigations that were queued during the
|
||||
// second pass to prevent infinite loops
|
||||
var i: usize = 0;
|
||||
while (i < navigations.items.len) {
|
||||
const page = navigations.items[i];
|
||||
if (page._queued_navigation) |qn| {
|
||||
if (qn.is_about_blank) {
|
||||
log.warn(.page, "recursive about blank", .{});
|
||||
_ = navigations.swapRemove(i);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn processFrameNavigation(self: *Session, page: *Page, qn: *QueuedNavigation) !void {
|
||||
lp.assert(page.parent != null, "root queued navigation", .{});
|
||||
|
||||
const browser = self.browser;
|
||||
const iframe = page.iframe.?;
|
||||
const parent = page.parent.?;
|
||||
|
||||
page._queued_navigation = null;
|
||||
defer browser.arena_pool.release(qn.arena);
|
||||
|
||||
const frame_id, const parent = blk: {
|
||||
const page = &self.page.?;
|
||||
const frame_id = page._frame_id;
|
||||
const parent = page.parent;
|
||||
errdefer iframe._window = null;
|
||||
|
||||
browser.http_client.abort();
|
||||
self.removePage();
|
||||
if (page._parent_notified) {
|
||||
// we already notified the parent that we had loaded
|
||||
parent._pending_loads += 1;
|
||||
}
|
||||
|
||||
break :blk .{ frame_id, parent };
|
||||
};
|
||||
const frame_id = page._frame_id;
|
||||
page.deinit(true);
|
||||
page.* = undefined;
|
||||
|
||||
self.page = @as(Page, undefined);
|
||||
const page = &self.page.?;
|
||||
try Page.init(page, frame_id, self, parent);
|
||||
errdefer page.deinit(true);
|
||||
|
||||
page.iframe = iframe;
|
||||
iframe._window = page.window;
|
||||
|
||||
page.navigate(qn.url, qn.opts) catch |err| {
|
||||
log.err(.browser, "queued frame navigation error", .{ .err = err });
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
fn processRootQueuedNavigation(self: *Session) !void {
|
||||
const current_page = &self.page.?;
|
||||
const frame_id = current_page._frame_id;
|
||||
|
||||
// create a copy before the page is cleared
|
||||
const qn = current_page._queued_navigation.?;
|
||||
current_page._queued_navigation = null;
|
||||
defer self.browser.arena_pool.release(qn.arena);
|
||||
|
||||
self.removePage();
|
||||
self.page = @as(Page, undefined);
|
||||
const new_page = &self.page.?;
|
||||
try Page.init(new_page, frame_id, self, null);
|
||||
|
||||
// Creates a new NavigationEventTarget for this page.
|
||||
try self.navigation.onNewPage(page);
|
||||
try self.navigation.onNewPage(new_page);
|
||||
|
||||
// start JS env
|
||||
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
|
||||
self.notification.dispatch(.page_created, page);
|
||||
self.notification.dispatch(.page_created, new_page);
|
||||
|
||||
page.navigate(qn.url, qn.opts) catch |err| {
|
||||
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
|
||||
new_page.navigate(qn.url, qn.opts) catch |err| {
|
||||
log.err(.browser, "queued navigation error", .{ .err = err });
|
||||
return err;
|
||||
};
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
pub fn nextFrameId(self: *Session) u32 {
|
||||
|
||||
@@ -153,7 +153,7 @@ pub fn fromIsolate(isolate: js.Isolate) *Context {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Context) void {
|
||||
if (comptime IS_DEBUG) {
|
||||
if (comptime IS_DEBUG and @import("builtin").is_test == false) {
|
||||
var it = self.unknown_properties.iterator();
|
||||
while (it.next()) |kv| {
|
||||
log.debug(.unknown_prop, "unknown property", .{
|
||||
|
||||
@@ -160,8 +160,8 @@ fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args
|
||||
try_catch.rethrow();
|
||||
return error.TryCatchRethrow;
|
||||
}
|
||||
caught.* = try_catch.caughtOrError(local.call_arena, error.JSExecCallback);
|
||||
return error.JSExecCallback;
|
||||
caught.* = try_catch.caughtOrError(local.call_arena, error.JsException);
|
||||
return error.JsException;
|
||||
};
|
||||
|
||||
if (@typeInfo(T) == .void) {
|
||||
|
||||
@@ -137,7 +137,7 @@ pub fn compileAndRun(self: *const Local, src: []const u8, name: ?[]const u8) !js
|
||||
) orelse return error.CompilationError;
|
||||
|
||||
// Run the script
|
||||
const result = v8.v8__Script__Run(v8_script, self.handle) orelse return error.ExecutionError;
|
||||
const result = v8.v8__Script__Run(v8_script, self.handle) orelse return error.JsException;
|
||||
return .{ .local = self, .handle = result };
|
||||
}
|
||||
|
||||
|
||||
@@ -72,3 +72,59 @@
|
||||
testing.expectEqual(2, calls);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id=fragment_clone_container></div>
|
||||
|
||||
<script id=clone_fragment>
|
||||
{
|
||||
let calls = 0;
|
||||
class MyFragmentCloneElement extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
calls += 1;
|
||||
$('#fragment_clone_container').appendChild(this);
|
||||
}
|
||||
}
|
||||
customElements.define('my-fragment-clone-element', MyFragmentCloneElement);
|
||||
|
||||
// Create a DocumentFragment with a custom element
|
||||
const fragment = document.createDocumentFragment();
|
||||
const customEl = document.createElement('my-fragment-clone-element');
|
||||
fragment.appendChild(customEl);
|
||||
|
||||
// Clone the fragment - this should trigger the crash
|
||||
// because the constructor will attach the element during cloning
|
||||
const clonedFragment = fragment.cloneNode(true);
|
||||
testing.expectEqual(2, calls);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id=range_clone_container></div>
|
||||
|
||||
<script id=clone_range>
|
||||
{
|
||||
let calls = 0;
|
||||
class MyRangeCloneElement extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
calls += 1;
|
||||
$('#range_clone_container').appendChild(this);
|
||||
}
|
||||
}
|
||||
customElements.define('my-range-clone-element', MyRangeCloneElement);
|
||||
|
||||
// Create a container with a custom element
|
||||
const container = document.createElement('div');
|
||||
const customEl = document.createElement('my-range-clone-element');
|
||||
container.appendChild(customEl);
|
||||
|
||||
// Create a range that includes the custom element
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(container);
|
||||
|
||||
// Clone the range contents - this should trigger the crash
|
||||
// because the constructor will attach the element during cloning
|
||||
const clonedContents = range.cloneContents();
|
||||
testing.expectEqual(2, calls);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -7,45 +7,59 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<iframe id=f1 onload="frame1Onload" src="support/sub 1.html"></iframe>
|
||||
<iframe id=f0></iframe>
|
||||
<iframe id=f1 onload="frame1Onload()" src="support/sub 1.html"></iframe>
|
||||
<iframe id=f2 src="support/sub2.html"></iframe>
|
||||
|
||||
<script id=empty>
|
||||
{
|
||||
const blank = document.createElement('iframe');
|
||||
testing.expectEqual(null, blank.contentDocument);
|
||||
document.documentElement.appendChild(blank);
|
||||
testing.expectEqual('<html><head></head><body></body></html>', blank.contentDocument.documentElement.outerHTML);
|
||||
|
||||
const f0 = $('#f0')
|
||||
testing.expectEqual('<html><head></head><body></body></html>', f0.contentDocument.documentElement.outerHTML);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="basic">
|
||||
// reload it
|
||||
$('#f2').src = 'support/sub2.html';
|
||||
testing.expectEqual(true, true);
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(undefined, window[10]);
|
||||
|
||||
testing.expectEqual(window, window[0].top);
|
||||
testing.expectEqual(window, window[0].parent);
|
||||
testing.expectEqual(false, window === window[0]);
|
||||
testing.expectEqual(undefined, window[20]);
|
||||
|
||||
testing.expectEqual(window, window[1].top);
|
||||
testing.expectEqual(window, window[1].parent);
|
||||
testing.expectEqual(false, window === window[1]);
|
||||
testing.expectEqual(false, window[0] === window[1]);
|
||||
|
||||
testing.expectEqual(window, window[2].top);
|
||||
testing.expectEqual(window, window[2].parent);
|
||||
testing.expectEqual(false, window === window[2]);
|
||||
testing.expectEqual(false, window[1] === window[2]);
|
||||
|
||||
testing.expectEqual(0, $('#f1').childNodes.length);
|
||||
|
||||
testing.expectEqual(testing.BASE_URL + 'frames/support/sub%201.html', $('#f1').src);
|
||||
testing.expectEqual(window[0], $('#f1').contentWindow);
|
||||
testing.expectEqual(window[1], $('#f2').contentWindow);
|
||||
testing.expectEqual(window[1], $('#f1').contentWindow);
|
||||
testing.expectEqual(window[2], $('#f2').contentWindow);
|
||||
|
||||
testing.expectEqual(window[0].document, $('#f1').contentDocument);
|
||||
testing.expectEqual(window[1].document, $('#f2').contentDocument);
|
||||
testing.expectEqual(window[1].document, $('#f1').contentDocument);
|
||||
testing.expectEqual(window[2].document, $('#f2').contentDocument);
|
||||
|
||||
// sibling frames share the same top
|
||||
testing.expectEqual(window[0].top, window[1].top);
|
||||
testing.expectEqual(window[1].top, window[2].top);
|
||||
|
||||
// child frames have no sub-frames
|
||||
testing.expectEqual(0, window[0].length);
|
||||
testing.expectEqual(0, window[1].length);
|
||||
testing.expectEqual(0, window[2].length);
|
||||
|
||||
// self and window are self-referential on child frames
|
||||
testing.expectEqual(window[0], window[0].self);
|
||||
testing.expectEqual(window[0], window[0].window);
|
||||
testing.expectEqual(window[1], window[1].self);
|
||||
testing.expectEqual(window[1], window[1].window);
|
||||
testing.expectEqual(window[2], window[2].self);
|
||||
|
||||
// child frame's top.parent is itself (root has no parent)
|
||||
testing.expectEqual(window, window[0].top.parent);
|
||||
@@ -62,6 +76,7 @@
|
||||
{
|
||||
let f3_load_event = false;
|
||||
let f3 = document.createElement('iframe');
|
||||
f3.id = 'f3';
|
||||
f3.addEventListener('load', () => {
|
||||
f3_load_event = true;
|
||||
});
|
||||
@@ -75,9 +90,10 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=onload>
|
||||
<script id=about_blank>
|
||||
{
|
||||
let f4 = document.createElement('iframe');
|
||||
f4.id = 'f4';
|
||||
f4.src = "about:blank";
|
||||
document.documentElement.appendChild(f4);
|
||||
|
||||
@@ -87,8 +103,43 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=count>
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(4, window.length);
|
||||
<script id=about_blank_renavigate>
|
||||
{
|
||||
let f5 = document.createElement('iframe');
|
||||
f5.id = 'f5';
|
||||
f5.src = "support/sub 1.html";
|
||||
document.documentElement.appendChild(f5);
|
||||
f5.src = "about:blank";
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual("<html><head></head><body></body></html>", f5.contentDocument.documentElement.outerHTML);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=link_click>
|
||||
testing.async(async (restore) => {
|
||||
await new Promise((resolve) => {
|
||||
let count = 0;
|
||||
let f6 = document.createElement('iframe');
|
||||
f6.id = 'f6';
|
||||
f6.addEventListener('load', () => {
|
||||
if (++count == 2) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
f6.contentDocument.querySelector('#link').click();
|
||||
});
|
||||
f6.src = "support/with_link.html";
|
||||
document.documentElement.appendChild(f6);
|
||||
});
|
||||
restore();
|
||||
testing.expectEqual("<html><head></head><body>It was clicked!\n</body></html>", f6.contentDocument.documentElement.outerHTML);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id=count>
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(8, window.length);
|
||||
});
|
||||
</script>
|
||||
|
||||
2
src/browser/tests/frames/support/after_link.html
Normal file
2
src/browser/tests/frames/support/after_link.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<!DOCTYPE html>
|
||||
It was clicked!
|
||||
2
src/browser/tests/frames/support/with_link.html
Normal file
2
src/browser/tests/frames/support/with_link.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<!DOCTYPE html>
|
||||
<a href="support/after_link.html" id=link>a link</a>
|
||||
@@ -40,6 +40,8 @@ const Selection = @import("Selection.zig");
|
||||
pub const XMLDocument = @import("XMLDocument.zig");
|
||||
pub const HTMLDocument = @import("HTMLDocument.zig");
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const Document = @This();
|
||||
|
||||
_type: Type,
|
||||
@@ -937,6 +939,32 @@ fn validateElementName(name: []const u8) !void {
|
||||
}
|
||||
}
|
||||
|
||||
// When a page or frame's URL is about:blank, or as soon as a frame is
|
||||
// programmatically created, it has this default "blank" content
|
||||
pub fn injectBlank(self: *Document, page: *Page) error{InjectBlankError}!void {
|
||||
self._injectBlank(page) catch |err| {
|
||||
// we wrap _injectBlank like this so that injectBlank can only return an
|
||||
// InjectBlankError. injectBlank is used in when nodes are inserted
|
||||
// as since it inserts node itself, Zig can't infer the error set.
|
||||
log.err(.browser, "inject blank", .{ .err = err });
|
||||
return error.InjectBlankError;
|
||||
};
|
||||
}
|
||||
|
||||
fn _injectBlank(self: *Document, page: *Page) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
// should only be called on an empty document
|
||||
std.debug.assert(self.asNode()._children == null);
|
||||
}
|
||||
|
||||
const html = try page.createElementNS(.html, "html", null);
|
||||
const head = try page.createElementNS(.html, "head", null);
|
||||
const body = try page.createElementNS(.html, "body", null);
|
||||
try page.appendNode(html, head, .{});
|
||||
try page.appendNode(html, body, .{});
|
||||
try page.appendNode(self.asNode(), html, .{});
|
||||
}
|
||||
|
||||
const ReadyState = enum {
|
||||
loading,
|
||||
interactive,
|
||||
|
||||
@@ -195,8 +195,9 @@ pub fn cloneFragment(self: *DocumentFragment, deep: bool, page: *Page) !*Node {
|
||||
|
||||
var child_it = node.childrenIterator();
|
||||
while (child_it.next()) |child| {
|
||||
const cloned_child = try child.cloneNode(true, page);
|
||||
try page.appendNode(fragment_node, cloned_child, .{ .child_already_connected = self_is_connected });
|
||||
if (try child.cloneNodeForAppending(true, page)) |cloned_child| {
|
||||
try page.appendNode(fragment_node, cloned_child, .{ .child_already_connected = self_is_connected });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1328,20 +1328,12 @@ pub fn clone(self: *Element, deep: bool, page: *Page) !*Node {
|
||||
if (deep) {
|
||||
var child_it = self.asNode().childrenIterator();
|
||||
while (child_it.next()) |child| {
|
||||
const cloned_child = try child.cloneNode(true, page);
|
||||
if (cloned_child._parent != null) {
|
||||
// This is almost always false, the only case where a cloned
|
||||
// node would already have a parent is with a custom element
|
||||
// that has a constructor (which is called during cloning) which
|
||||
// inserts it somewhere. In that case, whatever parent was set
|
||||
// in the constructor should not be changed.
|
||||
continue;
|
||||
if (try child.cloneNodeForAppending(true, page)) |cloned_child| {
|
||||
// We pass `true` to `child_already_connected` as a hacky optimization
|
||||
// We _know_ this child isn't connected (Because the parent isn't connected)
|
||||
// setting this to `true` skips all connection checks.
|
||||
try page.appendNode(node, cloned_child, .{ .child_already_connected = true });
|
||||
}
|
||||
|
||||
// We pass `true` to `child_already_connected` as a hacky optimization
|
||||
// We _know_ this child isn't connected (Because the parent isn't connected)
|
||||
// setting this to `true` skips all connection checks.
|
||||
try page.appendNode(node, cloned_child, .{ .child_already_connected = true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -180,8 +180,8 @@ pub fn getLocation(self: *const HTMLDocument) ?*@import("Location.zig") {
|
||||
return self._proto._location;
|
||||
}
|
||||
|
||||
pub fn setLocation(_: *const HTMLDocument, url: [:0]const u8, page: *Page) !void {
|
||||
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .script);
|
||||
pub fn setLocation(self: *HTMLDocument, url: [:0]const u8, page: *Page) !void {
|
||||
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = self._proto._page });
|
||||
}
|
||||
|
||||
pub fn getAll(self: *HTMLDocument, page: *Page) !*collections.HTMLAllCollection {
|
||||
|
||||
@@ -83,19 +83,19 @@ pub fn setHash(_: *const Location, hash: []const u8, page: *Page) !void {
|
||||
return page.scheduleNavigation(normalized_hash, .{
|
||||
.reason = .script,
|
||||
.kind = .{ .replace = null },
|
||||
}, .script);
|
||||
}, .{ .script = page });
|
||||
}
|
||||
|
||||
pub fn assign(_: *const Location, url: [:0]const u8, page: *Page) !void {
|
||||
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .script);
|
||||
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = page });
|
||||
}
|
||||
|
||||
pub fn replace(_: *const Location, url: [:0]const u8, page: *Page) !void {
|
||||
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .replace = null } }, .script);
|
||||
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .replace = null } }, .{ .script = page });
|
||||
}
|
||||
|
||||
pub fn reload(_: *const Location, page: *Page) !void {
|
||||
return page.scheduleNavigation(page.url, .{ .reason = .script, .kind = .reload }, .script);
|
||||
return page.scheduleNavigation(page.url, .{ .reason = .script, .kind = .reload }, .{ .script = page });
|
||||
}
|
||||
|
||||
pub fn toString(self: *const Location, page: *const Page) ![:0]const u8 {
|
||||
|
||||
@@ -724,6 +724,9 @@ const CloneError = error{
|
||||
TooManyContexts,
|
||||
LinkLoadError,
|
||||
StyleLoadError,
|
||||
TypeError,
|
||||
CompilationError,
|
||||
JsException,
|
||||
};
|
||||
pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) CloneError!*Node {
|
||||
const deep = deep_ orelse false;
|
||||
@@ -751,6 +754,29 @@ pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) CloneError!*Node {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clone a node for the purpose of appending to a parent.
|
||||
/// Returns null if the cloned node was already attached somewhere by a custom element
|
||||
/// constructor, indicating that the constructor's decision should be respected.
|
||||
///
|
||||
/// This helper is used when iterating over children to clone them. The typical pattern is:
|
||||
/// while (child_it.next()) |child| {
|
||||
/// if (try child.cloneNodeForAppending(true, page)) |cloned| {
|
||||
/// try page.appendNode(parent, cloned, opts);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// The only case where a cloned node would already have a parent is when a custom element
|
||||
/// constructor (which runs during cloning per the HTML spec) explicitly attaches the element
|
||||
/// somewhere. In that case, we respect the constructor's decision and return null to signal
|
||||
/// that the cloned node should not be appended to our intended parent.
|
||||
pub fn cloneNodeForAppending(self: *Node, deep: bool, page: *Page) CloneError!?*Node {
|
||||
const cloned = try self.cloneNode(deep, page);
|
||||
if (cloned._parent != null) {
|
||||
return null;
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
|
||||
pub fn compareDocumentPosition(self: *Node, other: *Node) u16 {
|
||||
const DISCONNECTED: u16 = 0x01;
|
||||
const PRECEDING: u16 = 0x02;
|
||||
|
||||
@@ -446,8 +446,9 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
|
||||
var offset = self._proto._start_offset;
|
||||
while (offset < self._proto._end_offset) : (offset += 1) {
|
||||
if (self._proto._start_container.getChildAt(offset)) |child| {
|
||||
const cloned = try child.cloneNode(true, page);
|
||||
_ = try fragment.asNode().appendChild(cloned, page);
|
||||
if (try child.cloneNodeForAppending(true, page)) |cloned| {
|
||||
_ = try fragment.asNode().appendChild(cloned, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -468,9 +469,11 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
|
||||
if (self._proto._start_container.parentNode() == self._proto._end_container.parentNode()) {
|
||||
var current = self._proto._start_container.nextSibling();
|
||||
while (current != null and current != self._proto._end_container) {
|
||||
const cloned = try current.?.cloneNode(true, page);
|
||||
_ = try fragment.asNode().appendChild(cloned, page);
|
||||
current = current.?.nextSibling();
|
||||
const next = current.?.nextSibling();
|
||||
if (try current.?.cloneNodeForAppending(true, page)) |cloned| {
|
||||
_ = try fragment.asNode().appendChild(cloned, page);
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -160,8 +160,8 @@ pub fn getSelection(self: *const Window) *Selection {
|
||||
return &self._document._selection;
|
||||
}
|
||||
|
||||
pub fn setLocation(_: *const Window, url: [:0]const u8, page: *Page) !void {
|
||||
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .script);
|
||||
pub fn setLocation(self: *Window, url: [:0]const u8, page: *Page) !void {
|
||||
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = self._page });
|
||||
}
|
||||
|
||||
pub fn getHistory(_: *Window, page: *Page) *History {
|
||||
|
||||
@@ -30,7 +30,7 @@ const IFrame = @This();
|
||||
_proto: *HtmlElement,
|
||||
_src: []const u8 = "",
|
||||
_executed: bool = false,
|
||||
_content_window: ?*Window = null,
|
||||
_window: ?*Window = null,
|
||||
|
||||
pub fn asElement(self: *IFrame) *Element {
|
||||
return self._proto._proto;
|
||||
@@ -40,11 +40,11 @@ pub fn asNode(self: *IFrame) *Node {
|
||||
}
|
||||
|
||||
pub fn getContentWindow(self: *const IFrame) ?*Window {
|
||||
return self._content_window;
|
||||
return self._window;
|
||||
}
|
||||
|
||||
pub fn getContentDocument(self: *const IFrame) ?*Document {
|
||||
const window = self._content_window orelse return null;
|
||||
const window = self._window orelse return null;
|
||||
return window._document;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,14 +72,6 @@ pub fn init(label_: ?[]const u8, opts_: ?InitOpts, page: *Page) !TextDecoderStre
|
||||
};
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *TextDecoderStream) void {
|
||||
self._transform.acquireRef();
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TextDecoderStream, shutdown: bool, page: *Page) void {
|
||||
self._transform.deinit(shutdown, page);
|
||||
}
|
||||
|
||||
fn decodeTransform(controller: *TransformStream.DefaultController, chunk: js.Value, ignoreBOM: bool) !void {
|
||||
// chunk should be a Uint8Array; decode it as UTF-8 string
|
||||
const typed_array = try chunk.toZig(js.TypedArray(u8));
|
||||
@@ -119,8 +111,6 @@ pub const JsApi = struct {
|
||||
pub const name = "TextDecoderStream";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const weak = true;
|
||||
pub const finalizer = bridge.finalizer(TextDecoderStream.deinit);
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(TextDecoderStream.init, .{});
|
||||
|
||||
@@ -34,14 +34,6 @@ pub fn init(page: *Page) !TextEncoderStream {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *TextEncoderStream) void {
|
||||
self._transform.acquireRef();
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TextEncoderStream, shutdown: bool, page: *Page) void {
|
||||
self._transform.deinit(shutdown, page);
|
||||
}
|
||||
|
||||
fn encodeTransform(controller: *TransformStream.DefaultController, chunk: js.Value) !void {
|
||||
// chunk should be a JS string; encode it as UTF-8 bytes (Uint8Array)
|
||||
const str = chunk.isString() orelse return error.InvalidChunk;
|
||||
@@ -64,8 +56,6 @@ pub const JsApi = struct {
|
||||
pub const name = "TextEncoderStream";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const weak = true;
|
||||
pub const finalizer = bridge.finalizer(TextEncoderStream.deinit);
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(TextEncoderStream.init, .{});
|
||||
|
||||
@@ -308,7 +308,7 @@ pub fn navigateInner(
|
||||
|
||||
_ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, true);
|
||||
} else {
|
||||
try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .script);
|
||||
try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .{ .script = page });
|
||||
}
|
||||
},
|
||||
.replace => |state| {
|
||||
@@ -321,7 +321,7 @@ pub fn navigateInner(
|
||||
|
||||
_ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, true);
|
||||
} else {
|
||||
try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .script);
|
||||
try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .{ .script = page });
|
||||
}
|
||||
},
|
||||
.traverse => |index| {
|
||||
@@ -334,11 +334,11 @@ pub fn navigateInner(
|
||||
// todo: Fire navigate event
|
||||
finished.resolve("navigation traverse", {});
|
||||
} else {
|
||||
try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .script);
|
||||
try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .{ .script = page });
|
||||
}
|
||||
},
|
||||
.reload => {
|
||||
try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .script);
|
||||
try page.scheduleNavigation(url, .{ .reason = .navigation, .kind = kind }, .{ .script = page });
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -52,8 +52,6 @@ _pull_fn: ?js.Function.Global = null,
|
||||
_pulling: bool = false,
|
||||
_pull_again: bool = false,
|
||||
_cancel: ?Cancel = null,
|
||||
_arena: std.mem.Allocator,
|
||||
_rc: usize = 0,
|
||||
|
||||
const UnderlyingSource = struct {
|
||||
start: ?js.Function = null,
|
||||
@@ -70,18 +68,13 @@ const QueueingStrategy = struct {
|
||||
pub fn init(src_: ?UnderlyingSource, strategy_: ?QueueingStrategy, page: *Page) !*ReadableStream {
|
||||
const strategy: QueueingStrategy = strategy_ orelse .{};
|
||||
|
||||
const arena = try page.getArena(.{ .debug = "ReadableStream" });
|
||||
errdefer page.releaseArena(arena);
|
||||
|
||||
const self = try arena.create(ReadableStream);
|
||||
self.* = .{
|
||||
const self = try page._factory.create(ReadableStream{
|
||||
._page = page,
|
||||
._state = .readable,
|
||||
._arena = arena,
|
||||
._reader = null,
|
||||
._controller = undefined,
|
||||
._stored_error = null,
|
||||
};
|
||||
});
|
||||
|
||||
self._controller = try ReadableStreamDefaultController.init(self, strategy.highWaterMark, page);
|
||||
|
||||
@@ -115,23 +108,6 @@ pub fn initWithData(data: []const u8, page: *Page) !*ReadableStream {
|
||||
return stream;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ReadableStream, _: bool, page: *Page) void {
|
||||
const rc = self._rc;
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(rc != 0);
|
||||
}
|
||||
|
||||
if (rc == 1) {
|
||||
page.releaseArena(self._arena);
|
||||
} else {
|
||||
self._rc = rc - 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *ReadableStream) void {
|
||||
self._rc += 1;
|
||||
}
|
||||
|
||||
pub fn getReader(self: *ReadableStream, page: *Page) !*ReadableStreamDefaultReader {
|
||||
if (self.getLocked()) {
|
||||
return error.ReaderLocked;
|
||||
@@ -144,12 +120,6 @@ pub fn getReader(self: *ReadableStream, page: *Page) !*ReadableStreamDefaultRead
|
||||
|
||||
pub fn releaseReader(self: *ReadableStream) void {
|
||||
self._reader = null;
|
||||
|
||||
const rc = self._rc;
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(rc != 0);
|
||||
}
|
||||
self._rc = rc - 1;
|
||||
}
|
||||
|
||||
pub fn getAsyncIterator(self: *ReadableStream, page: *Page) !*AsyncIterator {
|
||||
@@ -397,8 +367,6 @@ pub const JsApi = struct {
|
||||
pub const name = "ReadableStream";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const weak = true;
|
||||
pub const finalizer = bridge.finalizer(ReadableStream.deinit);
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(ReadableStream.init, .{});
|
||||
@@ -422,14 +390,6 @@ pub const AsyncIterator = struct {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *AsyncIterator) void {
|
||||
self._stream.acquireRef();
|
||||
}
|
||||
|
||||
pub fn deinit(self: *AsyncIterator, shutdown: bool, page: *Page) void {
|
||||
self._stream.deinit(shutdown, page);
|
||||
}
|
||||
|
||||
pub fn next(self: *AsyncIterator, page: *Page) !js.Promise {
|
||||
return self._reader.read(page);
|
||||
}
|
||||
@@ -446,8 +406,6 @@ pub const AsyncIterator = struct {
|
||||
pub const name = "ReadableStreamAsyncIterator";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const weak = true;
|
||||
pub const finalizer = bridge.finalizer(AsyncIterator.deinit);
|
||||
};
|
||||
|
||||
pub const next = bridge.function(ReadableStream.AsyncIterator.next, .{});
|
||||
|
||||
@@ -27,8 +27,6 @@ const ReadableStreamDefaultReader = @import("ReadableStreamDefaultReader.zig");
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
/// ReadableStreamDefaultController uses ReadableStream's arena to make
|
||||
/// allocation. Indeed, the controller is owned by its ReadableStream.
|
||||
const ReadableStreamDefaultController = @This();
|
||||
|
||||
pub const Chunk = union(enum) {
|
||||
@@ -48,6 +46,7 @@ pub const Chunk = union(enum) {
|
||||
|
||||
_page: *Page,
|
||||
_stream: *ReadableStream,
|
||||
_arena: std.mem.Allocator,
|
||||
_queue: std.ArrayList(Chunk),
|
||||
_pending_reads: std.ArrayList(js.PromiseResolver.Global),
|
||||
_high_water_mark: u32,
|
||||
@@ -57,22 +56,15 @@ pub fn init(stream: *ReadableStream, high_water_mark: u32, page: *Page) !*Readab
|
||||
._page = page,
|
||||
._queue = .empty,
|
||||
._stream = stream,
|
||||
._arena = page.arena,
|
||||
._pending_reads = .empty,
|
||||
._high_water_mark = high_water_mark,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *ReadableStreamDefaultController) void {
|
||||
self._stream.acquireRef();
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ReadableStreamDefaultController, shutdown: bool, page: *Page) void {
|
||||
self._stream.deinit(shutdown, page);
|
||||
}
|
||||
|
||||
pub fn addPendingRead(self: *ReadableStreamDefaultController, page: *Page) !js.Promise {
|
||||
const resolver = page.js.local.?.createPromiseResolver();
|
||||
try self._pending_reads.append(self._stream._arena, try resolver.persist());
|
||||
try self._pending_reads.append(self._arena, try resolver.persist());
|
||||
return resolver.promise();
|
||||
}
|
||||
|
||||
@@ -82,8 +74,8 @@ pub fn enqueue(self: *ReadableStreamDefaultController, chunk: Chunk) !void {
|
||||
}
|
||||
|
||||
if (self._pending_reads.items.len == 0) {
|
||||
const chunk_copy = try chunk.dupe(self._stream._arena);
|
||||
return self._queue.append(self._stream._arena, chunk_copy);
|
||||
const chunk_copy = try chunk.dupe(self._page.arena);
|
||||
return self._queue.append(self._arena, chunk_copy);
|
||||
}
|
||||
|
||||
// I know, this is ouch! But we expect to have very few (if any)
|
||||
@@ -117,7 +109,7 @@ pub fn enqueueValue(self: *ReadableStreamDefaultController, value: js.Value) !vo
|
||||
|
||||
if (self._pending_reads.items.len == 0) {
|
||||
const persisted = try value.persist();
|
||||
try self._queue.append(self._stream._arena, .{ .js_value = persisted });
|
||||
try self._queue.append(self._arena, .{ .js_value = persisted });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -178,7 +170,7 @@ pub fn doError(self: *ReadableStreamDefaultController, err: []const u8) !void {
|
||||
}
|
||||
|
||||
self._stream._state = .errored;
|
||||
self._stream._stored_error = try self._stream._arena.dupe(u8, err);
|
||||
self._stream._stored_error = try self._page.arena.dupe(u8, err);
|
||||
|
||||
// Reject all pending reads
|
||||
for (self._pending_reads.items) |resolver| {
|
||||
@@ -218,8 +210,6 @@ pub const JsApi = struct {
|
||||
pub const name = "ReadableStreamDefaultController";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const weak = true;
|
||||
pub const finalizer = bridge.finalizer(ReadableStreamDefaultController.deinit);
|
||||
};
|
||||
|
||||
pub const enqueue = bridge.function(ReadableStreamDefaultController.enqueueValue, .{});
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
const std = @import("std");
|
||||
const js = @import("../../js/js.zig");
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const Page = @import("../../Page.zig");
|
||||
const ReadableStream = @import("ReadableStream.zig");
|
||||
const ReadableStreamDefaultController = @import("ReadableStreamDefaultController.zig");
|
||||
@@ -37,21 +35,6 @@ pub fn init(stream: *ReadableStream, page: *Page) !*ReadableStreamDefaultReader
|
||||
});
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *ReadableStreamDefaultReader) void {
|
||||
const stream = self._stream orelse {
|
||||
if (comptime IS_DEBUG) {
|
||||
std.debug.assert(false);
|
||||
}
|
||||
return;
|
||||
};
|
||||
stream.acquireRef();
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ReadableStreamDefaultReader, shutdown: bool, page: *Page) void {
|
||||
const stream = self._stream orelse return;
|
||||
stream.deinit(shutdown, page);
|
||||
}
|
||||
|
||||
pub const ReadResult = struct {
|
||||
done: bool,
|
||||
value: Chunk,
|
||||
@@ -127,8 +110,6 @@ pub const JsApi = struct {
|
||||
pub const name = "ReadableStreamDefaultReader";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const weak = true;
|
||||
pub const finalizer = bridge.finalizer(ReadableStreamDefaultReader.deinit);
|
||||
};
|
||||
|
||||
pub const read = bridge.function(ReadableStreamDefaultReader.read, .{});
|
||||
|
||||
@@ -85,14 +85,6 @@ pub fn initWithZigTransform(zig_transform: ZigTransformFn, page: *Page) !*Transf
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *TransformStream) void {
|
||||
self._readable.acquireRef();
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TransformStream, shutdown: bool, page: *Page) void {
|
||||
self._readable.deinit(shutdown, page);
|
||||
}
|
||||
|
||||
pub fn transformWrite(self: *TransformStream, chunk: js.Value, page: *Page) !void {
|
||||
if (self._controller._zig_transform_fn) |zig_fn| {
|
||||
// Zig-level transform (used by TextEncoderStream etc.)
|
||||
@@ -138,8 +130,6 @@ pub const JsApi = struct {
|
||||
pub const name = "TransformStream";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const weak = true;
|
||||
pub const finalizer = bridge.finalizer(TransformStream.deinit);
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(TransformStream.init, .{});
|
||||
@@ -175,14 +165,6 @@ pub const TransformStreamDefaultController = struct {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *TransformStreamDefaultController) void {
|
||||
self._stream.acquireRef();
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TransformStreamDefaultController, shutdown: bool, page: *Page) void {
|
||||
self._stream.deinit(shutdown, page);
|
||||
}
|
||||
|
||||
pub fn enqueue(self: *TransformStreamDefaultController, chunk: ReadableStreamDefaultController.Chunk) !void {
|
||||
try self._stream._readable._controller.enqueue(chunk);
|
||||
}
|
||||
@@ -207,8 +189,6 @@ pub const TransformStreamDefaultController = struct {
|
||||
pub const name = "TransformStreamDefaultController";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const weak = true;
|
||||
pub const finalizer = bridge.finalizer(TransformStreamDefaultController.deinit);
|
||||
};
|
||||
|
||||
pub const enqueue = bridge.function(TransformStreamDefaultController.enqueueValue, .{});
|
||||
|
||||
Reference in New Issue
Block a user