diff --git a/src/Inbox.zig b/src/Inbox.zig
index 1828f0f6..0064d2aa 100644
--- a/src/Inbox.zig
+++ b/src/Inbox.zig
@@ -73,6 +73,21 @@ pub fn pop(self: *Inbox) ?*Message {
return @fieldParentPtr("node", node);
}
+// Peek for a message matching `predicate` without removing it. Used by
+// syncRequest to notice a queued teardown command (which sync_wait can't
+// safely dispatch mid-parse) so it can abort the blocking fetch instead
+// of stalling for the full per-request timeout.
+pub fn contains(self: *Inbox, predicate: *const fn (*Message) bool) bool {
+ self.mutex.lock();
+ defer self.mutex.unlock();
+ var it = self.queue.first;
+ while (it) |node| : (it = node.next) {
+ const msg: *Message = @fieldParentPtr("node", node);
+ if (predicate(msg)) return true;
+ }
+ return false;
+}
+
// Cherry-pick the first message for which `predicate(msg)` returns
// true, removing it from the queue. Walks the queue in FIFO order;
// non-matching messages stay in place. Used to dispatch only the
diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig
index 884a5f88..ec08fab0 100644
--- a/src/browser/Frame.zig
+++ b/src/browser/Frame.zig
@@ -36,6 +36,7 @@ const CustomElementReactions = @import("CustomElementReactions.zig");
const URL = @import("URL.zig");
const Blob = @import("webapi/Blob.zig");
+const FileList = @import("webapi/FileList.zig");
const Node = @import("webapi/Node.zig");
const Event = @import("webapi/Event.zig");
const EventTarget = @import("webapi/EventTarget.zig");
@@ -57,9 +58,11 @@ const CSSStyleSheet = @import("webapi/css/CSSStyleSheet.zig");
const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig");
const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
const SubmitEvent = @import("webapi/event/SubmitEvent.zig");
+const popover = @import("webapi/element/popover.zig");
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
const MouseEvent = @import("webapi/event/MouseEvent.zig");
+const WheelEvent = @import("webapi/event/WheelEvent.zig");
const HttpClient = @import("HttpClient.zig");
@@ -145,6 +148,10 @@ _event_target_attr_listeners: GlobalEventHandlersLookup = .empty,
// Blob URL registry for URL.createObjectURL/revokeObjectURL
_blob_urls: std.StringHashMapUnmanaged(*Blob) = .{},
+// FileLists owned by `` elements. Each holds refs on its
+// File objects (reference counted via their Blob proto); released at teardown.
+_file_lists: std.ArrayList(*FileList) = .{},
+
/// `load` events that'll be fired before window's `load` event.
/// A call to `documentIsComplete` (which calls `_documentIsComplete`) resets it.
/// Double-buffered so that dispatching load events (which may trigger JS that
@@ -403,6 +410,12 @@ pub fn deinit(self: *Frame) void {
}
}
+ for (self._file_lists.items) |file_list| {
+ for (file_list._files) |file| {
+ file._proto.releaseRef(page);
+ }
+ }
+
{
var node: ?*std.DoublyLinkedList.Node = self._mutation_observers.first;
while (node) |n| {
@@ -887,7 +900,10 @@ pub fn abortTransfers(self: *Frame) void {
for (self.child_frames.items) |child| {
child.abortTransfers();
}
- self._session.browser.http_client.abortOwner(&self._http_owner);
+ const http_client = &self._session.browser.http_client;
+ http_client.abortOwner(&self._http_owner);
+ // abortOwner misses deferred contexts whose transfer already completed.
+ http_client.deferring_layer.cancelFrame(self._frame_id);
}
pub fn documentIsLoaded(self: *Frame) void {
@@ -1646,6 +1662,11 @@ pub fn registerIntersectionObserver(self: *Frame, observer: *IntersectionObserve
try self._intersection_observers.append(self.arena, observer);
}
+// Tracks a file input's FileList so its File refs are released at teardown.
+pub fn trackFileList(self: *Frame, file_list: *FileList) !void {
+ try self._file_lists.append(self.arena, file_list);
+}
+
pub fn unregisterIntersectionObserver(self: *Frame, observer: *IntersectionObserver) void {
for (self._intersection_observers.items, 0..) |obs, i| {
if (obs == observer) {
@@ -3044,24 +3065,11 @@ pub fn removeNode(self: *Frame, parent: *Node, child: *Node, opts: RemoveNodeOpt
null;
const children = parent._children.?;
- switch (children.*) {
- .one => |n| {
- lp.assert(n == child, "Frame.removeNode.one", .{});
- parent._children = null;
- self._factory.destroy(children);
- },
- .list => |list| {
- list.remove(&child._child_link);
-
- // Should not be possible to get a child list with a single node.
- // While it doesn't cause any problems, it indicates an bug in the
- // code as these should always be represented as .{.one = node}
- const first = list.first.?;
- if (first.next == null) {
- children.* = .{ .one = Node.linkToNode(first) };
- self._factory.destroy(list);
- }
- },
+ children.remove(&child._child_link);
+ if (children.first == null) {
+ // last child removed; drop the list so a childless node holds no allocation
+ parent._children = null;
+ self._factory.destroy(children);
}
// grab this before we null the parent
const was_connected = child.isConnected();
@@ -3127,6 +3135,8 @@ pub fn removeNode(self: *Frame, parent: *Node, child: *Node, opts: RemoveNodeOpt
Element.Html.Custom.enqueueDisconnectedCallbackOnElement(el, self);
+ popover.removeFromOpen(el, self);
+
// If a