mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 09:35:59 -04:00
Merge branch 'main' into agent
This commit is contained in:
23
.github/workflows/zig-test.yml
vendored
23
.github/workflows/zig-test.yml
vendored
@@ -50,29 +50,11 @@ jobs:
|
||||
|
||||
- name: Run zig fmt
|
||||
id: fmt
|
||||
run: |
|
||||
zig fmt --check ./*.zig ./**/*.zig 2> zig-fmt.err > zig-fmt.err2 || echo "Failed"
|
||||
delimiter="$(openssl rand -hex 8)"
|
||||
echo "zig_fmt_errs<<${delimiter}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
if [ -s zig-fmt.err ]; then
|
||||
echo "// The following errors occurred:" >> "${GITHUB_OUTPUT}"
|
||||
cat zig-fmt.err >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
if [ -s zig-fmt.err2 ]; then
|
||||
echo "// The following files were not formatted:" >> "${GITHUB_OUTPUT}"
|
||||
cat zig-fmt.err2 >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
echo "${delimiter}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Fail the job
|
||||
if: steps.fmt.outputs.zig_fmt_errs != ''
|
||||
run: exit 1
|
||||
run: zig fmt --check ./
|
||||
|
||||
zig-test-debug:
|
||||
name: zig test using v8 in debug mode
|
||||
needs: zig-fmt
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
@@ -93,6 +75,7 @@ jobs:
|
||||
|
||||
zig-test-release:
|
||||
name: zig test
|
||||
needs: zig-fmt
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
@@ -243,9 +243,6 @@ child_frames: std.ArrayList(*Frame) = .{},
|
||||
// Workers created by this frame. Cleaned up when frame is destroyed.
|
||||
workers: std.ArrayList(*Worker) = .{},
|
||||
|
||||
// DOM version used to invalidate cached state of "live" collections
|
||||
version: usize = 0,
|
||||
|
||||
// This is maybe not great. It's a counter on the number of events that we're
|
||||
// waiting on before triggering the "load" event. Essentially, we need all
|
||||
// synchronous scripts and all iframes to be loaded. Scripts are handled by the
|
||||
@@ -1441,7 +1438,7 @@ pub fn openPopup(self: *Frame, opts: OpenPopupOpts) !*Frame {
|
||||
}
|
||||
|
||||
pub fn domChanged(self: *Frame) void {
|
||||
self.version += 1;
|
||||
self._page.dom_version += 1;
|
||||
|
||||
if (self._intersection_check_scheduled) {
|
||||
return;
|
||||
@@ -3008,7 +3005,7 @@ pub fn appendNode(self: *Frame, parent: *Node, child: *Node, opts: InsertNodeOpt
|
||||
}
|
||||
|
||||
pub fn appendAllChildren(self: *Frame, parent: *Node, target: *Node) !void {
|
||||
target.bumpDomVersion(self);
|
||||
self.domChanged();
|
||||
const dest_connected = target.isConnected();
|
||||
|
||||
// Use firstChild() instead of iterator to handle cases where callbacks
|
||||
@@ -3023,7 +3020,7 @@ pub fn appendAllChildren(self: *Frame, parent: *Node, target: *Node) !void {
|
||||
}
|
||||
|
||||
pub fn insertAllChildrenBefore(self: *Frame, fragment: *Node, parent: *Node, ref_node: *Node) !void {
|
||||
parent.bumpDomVersion(self);
|
||||
self.domChanged();
|
||||
const dest_connected = parent.isConnected();
|
||||
|
||||
// Use firstChild() instead of iterator to handle cases where callbacks
|
||||
|
||||
@@ -205,7 +205,9 @@ pub fn init(self: *Client, allocator: Allocator, network: *Network, cdp_client:
|
||||
next = layerWith(&self.cache_layer, next);
|
||||
}
|
||||
|
||||
next = layerWith(&self.interception_layer, next);
|
||||
if (network.config.mode == .serve) {
|
||||
next = layerWith(&self.interception_layer, next);
|
||||
}
|
||||
|
||||
if (network.config.webBotAuth() != null) {
|
||||
next = layerWith(&self.web_bot_auth_layer, next);
|
||||
|
||||
@@ -45,6 +45,19 @@ const Page = @This();
|
||||
|
||||
session: *Session,
|
||||
|
||||
// DOM version used to invalidate cached state of "live" collections. Ideally
|
||||
// this would be on the Frame (and that's where it used to be). But getting the
|
||||
// frame from a DOM mutation call is [relatively] expensive. You can't use
|
||||
// the bridge-injected *Frame, because that's the frame where the JS is being
|
||||
// executed, which might not be the *Frame that owns the node. We don't store
|
||||
// *Frame in node (think of the memory!), so we have to iterate through its
|
||||
// parents, find the Document, which has the frame.
|
||||
// So the choice is between making every DOM mutation (which has to increase
|
||||
// the dom_version) + every read (which has to check the version) slow, or
|
||||
// putting this on the Page, and having an DOM mutation in Frame 1 invalidate
|
||||
// a cached lookup on Frame 2. We picked the latter.
|
||||
dom_version: usize = 0,
|
||||
|
||||
// DOM object factory scoped to this Page's documents.
|
||||
factory: Factory,
|
||||
|
||||
|
||||
@@ -233,7 +233,6 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
||||
defer self.base.is_evaluating = was_evaluating;
|
||||
|
||||
const headers = try self.getHeaders();
|
||||
errdefer headers.deinit();
|
||||
|
||||
if (is_blocking) {
|
||||
const response = try self.base.client.syncRequest(arena, .{
|
||||
|
||||
@@ -181,7 +181,7 @@ fn getThis(self: *const Function) js.Object {
|
||||
}
|
||||
|
||||
pub fn src(self: *const Function) ![]const u8 {
|
||||
return self.local.valueToString(.{ .local = self.local, .handle = @ptrCast(self.handle) }, .{});
|
||||
return js.Value.toStringSlice(.{ .local = self.local, .handle = @ptrCast(self.handle) });
|
||||
}
|
||||
|
||||
pub fn getPropertyValue(self: *const Function, name: []const u8) !?js.Value {
|
||||
|
||||
@@ -516,7 +516,25 @@ fn countInternalFields(comptime JsApi: type) u8 {
|
||||
// Shared illegal constructor callback for types without explicit constructors
|
||||
fn illegalConstructorCallback(raw_info: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(raw_info);
|
||||
log.warn(.js, "Illegal constructor call", .{});
|
||||
|
||||
// Recover the constructor's name via NewTarget, whose .name property was
|
||||
// set via SetClassName when the FunctionTemplate was built. Lets us tell
|
||||
// `new DOMException()` apart from `new MutationRecord()` in the warning.
|
||||
var name_buf: [128]u8 = undefined;
|
||||
var name: []const u8 = "<unknown>";
|
||||
if (v8.v8__FunctionCallbackInfo__NewTarget(raw_info)) |new_target| {
|
||||
if (v8.v8__Value__IsFunction(new_target)) {
|
||||
const func: *const v8.Function = @ptrCast(new_target);
|
||||
if (v8.v8__Function__GetName(func)) |name_value| {
|
||||
if (v8.v8__Value__IsString(name_value)) {
|
||||
const str: *const v8.String = @ptrCast(name_value);
|
||||
const n = v8.v8__String__WriteUtf8(str, isolate, &name_buf, name_buf.len, v8.NO_NULL_TERMINATION | v8.REPLACE_INVALID_UTF8);
|
||||
name = name_buf[0..@intCast(n)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info(.js, "Illegal constructor call", .{ .name = name });
|
||||
|
||||
const message = v8.v8__String__NewFromUtf8(isolate, "Illegal Constructor", v8.kNormal, 19);
|
||||
const js_exception = v8.v8__Exception__TypeError(message);
|
||||
@@ -633,6 +651,7 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
|
||||
const cb = v8.v8__FunctionTemplate__New__Config(isolate, &.{
|
||||
.callback = setter,
|
||||
.signature = getter_signature,
|
||||
.length = 1,
|
||||
}).?;
|
||||
const setter_name_str = "set " ++ name;
|
||||
const setter_name_v8 = v8.v8__String__NewFromUtf8(isolate, setter_name_str.ptr, v8.kNormal, @intCast(setter_name_str.len));
|
||||
@@ -673,9 +692,12 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
|
||||
}).?;
|
||||
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||
v8.v8__FunctionTemplate__SetClassName(function_template, js_name);
|
||||
if (value.static) {
|
||||
if (value.static and !own_properties) {
|
||||
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
|
||||
} else {
|
||||
// For own_properties namespaces, static methods still belong
|
||||
// on the instance — `CSS` is exposed as an instance via
|
||||
// `window.CSS`, not as a constructor.
|
||||
v8.v8__Template__Set(@ptrCast(member_template), js_name, @ptrCast(function_template), v8.None);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -153,7 +153,9 @@ pub const Function = struct {
|
||||
.cache = opts.cache,
|
||||
.static = opts.static,
|
||||
.wpt_only = opts.wpt_only,
|
||||
.arity = getArity(@TypeOf(func), 1),
|
||||
// Non-static methods receive `self` as their first param; static
|
||||
// methods don't, so don't skip the first param for them.
|
||||
.arity = getArity(@TypeOf(func), if (opts.static) 0 else 1),
|
||||
.func = if (opts.noop) noopFunction else struct {
|
||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
Caller.Function.call(T, handle.?, func, opts);
|
||||
|
||||
@@ -82,3 +82,18 @@
|
||||
testing.expectEqual('\uFFFDabc', CSS.escape('\x00abc'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="static_no_this_binding">
|
||||
// react-jss extracts CSS.escape and calls it with no `this`. Per spec these
|
||||
// are static namespace methods, so the call must not throw Illegal invocation.
|
||||
{
|
||||
const escape = CSS.escape;
|
||||
testing.expectEqual('foo\\#bar', escape('foo#bar'));
|
||||
testing.expectEqual('foo\\#bar', escape.call(null, 'foo#bar'));
|
||||
testing.expectEqual('foo\\#bar', escape.call(undefined, 'foo#bar'));
|
||||
testing.expectEqual('foo\\#bar', escape.call({}, 'foo#bar'));
|
||||
|
||||
const supports = CSS.supports;
|
||||
testing.expectEqual(true, supports('display', 'block'));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -40,7 +40,7 @@ pub fn parseDimension(value: []const u8) ?f64 {
|
||||
|
||||
/// Escapes a CSS identifier string
|
||||
/// https://drafts.csswg.org/cssom/#the-css.escape()-method
|
||||
pub fn escape(_: *const CSS, value: []const u8, frame: *Frame) ![]const u8 {
|
||||
pub fn escape(value: []const u8, frame: *Frame) ![]const u8 {
|
||||
if (value.len == 0) {
|
||||
return "";
|
||||
}
|
||||
@@ -92,7 +92,7 @@ pub fn escape(_: *const CSS, value: []const u8, frame: *Frame) ![]const u8 {
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn supports(_: *const CSS, property_or_condition: []const u8, value: ?[]const u8) bool {
|
||||
pub fn supports(property_or_condition: []const u8, value: ?[]const u8) bool {
|
||||
_ = property_or_condition;
|
||||
_ = value;
|
||||
return true;
|
||||
@@ -175,8 +175,8 @@ pub const JsApi = struct {
|
||||
pub const empty_with_no_proto = true;
|
||||
};
|
||||
|
||||
pub const escape = bridge.function(CSS.escape, .{});
|
||||
pub const supports = bridge.function(CSS.supports, .{});
|
||||
pub const escape = bridge.function(CSS.escape, .{ .static = true });
|
||||
pub const supports = bridge.function(CSS.supports, .{ .static = true });
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
@@ -489,7 +489,7 @@ pub fn append(self: *Document, nodes: []const Node.NodeOrText, frame: *Frame) !v
|
||||
try validateDocumentNodes(self, nodes, false);
|
||||
|
||||
const parent = self.asNode();
|
||||
parent.bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
const parent_is_connected = parent.isConnected();
|
||||
|
||||
for (nodes) |node_or_text| {
|
||||
@@ -514,7 +514,7 @@ pub fn prepend(self: *Document, nodes: []const Node.NodeOrText, frame: *Frame) !
|
||||
try validateDocumentNodes(self, nodes, false);
|
||||
|
||||
const parent = self.asNode();
|
||||
parent.bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
const parent_is_connected = parent.isConnected();
|
||||
|
||||
var i = nodes.len;
|
||||
@@ -736,7 +736,7 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool
|
||||
insert_after = child;
|
||||
}
|
||||
|
||||
parent.bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
self._write_insertion_point = children_to_insert.getLast();
|
||||
}
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ pub fn getInnerHTML(self: *DocumentFragment, writer: *std.Io.Writer, frame: *Fra
|
||||
pub fn setInnerHTML(self: *DocumentFragment, html: []const u8, frame: *Frame) !void {
|
||||
const parent = self.asNode();
|
||||
|
||||
parent.bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
var it = parent.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
frame.removeNode(parent, child, .{ .will_be_reconnected = false });
|
||||
|
||||
@@ -467,7 +467,7 @@ pub fn setOuterHTML(self: *Element, html: []const u8, frame: *Frame) !void {
|
||||
const node = self.asNode();
|
||||
const parent = node._parent orelse return;
|
||||
|
||||
parent.bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
if (html.len > 0) {
|
||||
const fragment = (try Node.DocumentFragment.init(frame)).asNode();
|
||||
try frame.parseHtmlAsChildren(fragment, html);
|
||||
@@ -493,7 +493,7 @@ pub fn setInnerHTML(self: *Element, html: []const u8, frame: *Frame) !void {
|
||||
// fires disconnectedCallback for custom elements, which can mutate
|
||||
// the child list and dangle any cached next-pointer the iterator
|
||||
// would otherwise hold.
|
||||
parent.bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
while (parent.firstChild()) |child| {
|
||||
frame.removeNode(parent, child, .{ .will_be_reconnected = false });
|
||||
}
|
||||
@@ -881,7 +881,7 @@ pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, frame: *F
|
||||
pub fn replaceWith(self: *Element, nodes: []const Node.NodeOrText, frame: *Frame) !void {
|
||||
const ref_node = self.asNode();
|
||||
const parent = ref_node._parent orelse return;
|
||||
parent.bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
|
||||
const parent_is_connected = parent.isConnected();
|
||||
|
||||
@@ -920,7 +920,7 @@ pub fn replaceWith(self: *Element, nodes: []const Node.NodeOrText, frame: *Frame
|
||||
pub fn remove(self: *Element, frame: *Frame) void {
|
||||
const node = self.asNode();
|
||||
const parent = node._parent orelse return;
|
||||
parent.bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
frame.removeNode(parent, node, .{ .will_be_reconnected = false });
|
||||
}
|
||||
|
||||
|
||||
@@ -203,6 +203,9 @@ pub const JsApi = struct {
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "WebApi: EventTarget" {
|
||||
const filter: testing.LogFilter = .init(&.{ .js, .event });
|
||||
defer filter.deinit();
|
||||
|
||||
// we create thousands of these per frame. Nothing should bloat it.
|
||||
try testing.expectEqual(16, @sizeOf(EventTarget));
|
||||
try testing.htmlRunner("events.html", .{});
|
||||
|
||||
@@ -226,7 +226,7 @@ pub fn appendChild(self: *Node, child: *Node, frame: *Frame) !*Node {
|
||||
|
||||
try validateNodeInsertion(self, child);
|
||||
|
||||
self.bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
|
||||
// If the child is currently connected, and if its new parent is connected,
|
||||
// then we can remove + add a bit more efficiently (we don't have to fully
|
||||
@@ -532,15 +532,6 @@ pub fn ownerFrame(self: *const Node, default: *Frame) *Frame {
|
||||
return doc._frame orelse default;
|
||||
}
|
||||
|
||||
// Tells the owning frame that this node's subtree has changed. Use this from
|
||||
// mutation paths instead of `frame.domChanged()` so cross-realm mutations
|
||||
// (e.g., parent JS mutating an iframe's DOM) bump the iframe's version, not
|
||||
// the caller's. Otherwise, live collections that key off the owning frame's
|
||||
// version won't see the change and will return stale cached state.
|
||||
pub fn bumpDomVersion(self: *const Node, default: *Frame) void {
|
||||
self.ownerFrame(default).domChanged();
|
||||
}
|
||||
|
||||
pub const ResolveURLOpts = struct {
|
||||
allocator: ?Allocator = null,
|
||||
};
|
||||
@@ -572,7 +563,7 @@ pub fn removeChild(self: *Node, child: *Node, frame: *Frame) !*Node {
|
||||
var it = self.childrenIterator();
|
||||
while (it.next()) |n| {
|
||||
if (n == child) {
|
||||
self.bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
frame.removeNode(self, child, .{ .will_be_reconnected = false });
|
||||
return child;
|
||||
}
|
||||
@@ -587,7 +578,7 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, frame: *Fra
|
||||
|
||||
// special case: if nodes are the same, ignore the change.
|
||||
if (new_node == ref_node_) {
|
||||
self.bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
|
||||
if (frame.hasMutationObservers()) {
|
||||
const parent = new_node._parent.?;
|
||||
@@ -618,7 +609,7 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, frame: *Fra
|
||||
const parent_owner = self.ownerDocument(frame) orelse self.as(Document);
|
||||
const adopting_to_new_document = child_owner != null and child_owner.? != parent_owner;
|
||||
|
||||
self.bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
const will_be_reconnected = self.isConnected() and !adopting_to_new_document;
|
||||
if (new_node._parent) |parent| {
|
||||
frame.removeNode(parent, new_node, .{ .will_be_reconnected = will_be_reconnected });
|
||||
@@ -1089,7 +1080,7 @@ pub fn replaceChildren(self: *Node, nodes: []const NodeOrText, frame: *Frame) !v
|
||||
}
|
||||
}
|
||||
|
||||
self.bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
|
||||
// Remove all existing children
|
||||
var it = self.childrenIterator();
|
||||
|
||||
@@ -374,7 +374,7 @@ pub fn deleteContents(self: *Range, frame: *Frame) !void {
|
||||
if (self._proto.getCollapsed()) {
|
||||
return;
|
||||
}
|
||||
self._proto._start_container.bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
|
||||
// Simple case: same container
|
||||
if (self._proto._start_container == self._proto._end_container) {
|
||||
|
||||
@@ -34,11 +34,8 @@ _arena: std.mem.Allocator,
|
||||
_last_index: usize,
|
||||
_last_length: ?u32,
|
||||
_last_node: ?*std.DoublyLinkedList.Node,
|
||||
// Version observed on `_owning_frame` the last time we refreshed our cache.
|
||||
// Compare against `_owning_frame.version` to detect DOM mutations.
|
||||
_cached_version: usize,
|
||||
_node: *Node,
|
||||
_owning_frame: *Frame,
|
||||
|
||||
pub const KeyIterator = GenericIterator(Iterator, "0");
|
||||
pub const ValueIterator = GenericIterator(Iterator, "1");
|
||||
@@ -48,8 +45,6 @@ pub fn init(node: *Node, frame: *Frame) !*ChildNodes {
|
||||
const arena = try frame.getArena(.small, "ChildNodes");
|
||||
errdefer frame.releaseArena(arena);
|
||||
|
||||
const owning_frame = node.ownerFrame(frame);
|
||||
|
||||
const self = try arena.create(ChildNodes);
|
||||
self.* = .{
|
||||
._node = node,
|
||||
@@ -57,8 +52,7 @@ pub fn init(node: *Node, frame: *Frame) !*ChildNodes {
|
||||
._last_index = 0,
|
||||
._last_node = null,
|
||||
._last_length = null,
|
||||
._cached_version = owning_frame.version,
|
||||
._owning_frame = owning_frame,
|
||||
._cached_version = frame._page.dom_version,
|
||||
};
|
||||
return self;
|
||||
}
|
||||
@@ -67,8 +61,8 @@ pub fn deinit(self: *const ChildNodes, page: *Page) void {
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn length(self: *ChildNodes, _: *Frame) !u32 {
|
||||
if (self.versionCheck()) {
|
||||
pub fn length(self: *ChildNodes, frame: *const Frame) !u32 {
|
||||
if (self.versionCheck(frame)) {
|
||||
if (self._last_length) |cached_length| {
|
||||
return cached_length;
|
||||
}
|
||||
@@ -81,8 +75,8 @@ pub fn length(self: *ChildNodes, _: *Frame) !u32 {
|
||||
return len;
|
||||
}
|
||||
|
||||
pub fn getAtIndex(self: *ChildNodes, index: usize, _: *Frame) !?*Node {
|
||||
_ = self.versionCheck();
|
||||
pub fn getAtIndex(self: *ChildNodes, index: usize, frame: *const Frame) !?*Node {
|
||||
_ = self.versionCheck(frame);
|
||||
|
||||
var current = self._last_index;
|
||||
var node: ?*std.DoublyLinkedList.Node = null;
|
||||
@@ -122,8 +116,8 @@ pub fn entries(self: *ChildNodes, frame: *Frame) !*EntryIterator {
|
||||
return .init(.{ .list = self }, frame);
|
||||
}
|
||||
|
||||
fn versionCheck(self: *ChildNodes) bool {
|
||||
const current = self._owning_frame.version;
|
||||
fn versionCheck(self: *ChildNodes, frame: *const Frame) bool {
|
||||
const current = frame._page.dom_version;
|
||||
if (current == self._cached_version) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -32,21 +32,18 @@ _tw: TreeWalker.FullExcludeSelf,
|
||||
_last_index: usize,
|
||||
_last_length: ?u32,
|
||||
_cached_version: usize,
|
||||
_owning_frame: *Frame,
|
||||
|
||||
pub fn init(root: *Node, frame: *Frame) HTMLAllCollection {
|
||||
const owning_frame = root.ownerFrame(frame);
|
||||
return .{
|
||||
._last_index = 0,
|
||||
._last_length = null,
|
||||
._tw = TreeWalker.FullExcludeSelf.init(root, .{}),
|
||||
._cached_version = owning_frame.version,
|
||||
._owning_frame = owning_frame,
|
||||
._cached_version = frame._page.dom_version,
|
||||
};
|
||||
}
|
||||
|
||||
fn versionCheck(self: *HTMLAllCollection, _: *const Frame) bool {
|
||||
const current = self._owning_frame.version;
|
||||
fn versionCheck(self: *HTMLAllCollection, frame: *const Frame) bool {
|
||||
const current = frame._page.dom_version;
|
||||
if (self._cached_version != current) {
|
||||
self._cached_version = current;
|
||||
self._last_index = 0;
|
||||
|
||||
@@ -79,7 +79,7 @@ const Filters = union(Mode) {
|
||||
// But, if the version hasn't changed, then we can leverage other stateful data
|
||||
// to improve performance. For example, we cache the length property. So once
|
||||
// we've walked the tree to figure the length, we can re-use the cached property
|
||||
// if the DOM is unchanged (i.e. if our _cached_version == frame.version).
|
||||
// if the DOM is unchanged (i.e. if our _cached_version == page.dom_version).
|
||||
//
|
||||
// We do something similar for indexed getter (e.g. coll[4]), by preserving the
|
||||
// last node visited in the tree (implicitly by not resetting the TreeWalker).
|
||||
@@ -99,19 +99,16 @@ pub fn NodeLive(comptime mode: Mode) type {
|
||||
_last_index: usize,
|
||||
_last_length: ?u32,
|
||||
_cached_version: usize,
|
||||
_owning_frame: *Frame,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(root: *Node, filter: Filter, frame: *Frame) Self {
|
||||
const owning_frame = root.ownerFrame(frame);
|
||||
return .{
|
||||
._last_index = 0,
|
||||
._last_length = null,
|
||||
._filter = filter,
|
||||
._tw = TW.init(root, .{}),
|
||||
._cached_version = owning_frame.version,
|
||||
._owning_frame = owning_frame,
|
||||
._cached_version = frame._page.dom_version,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -345,8 +342,8 @@ pub fn NodeLive(comptime mode: Mode) type {
|
||||
};
|
||||
}
|
||||
|
||||
fn versionCheck(self: *Self, _: *const Frame) bool {
|
||||
const current = self._owning_frame.version;
|
||||
fn versionCheck(self: *Self, frame: *const Frame) bool {
|
||||
const current = frame._page.dom_version;
|
||||
if (current == self._cached_version) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ pub const List = struct {
|
||||
};
|
||||
try frame.addElementId(parent, element, entry._value.str());
|
||||
}
|
||||
element.asNode().bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
frame.attributeChange(element, result.normalized, entry._value, old_value);
|
||||
return entry;
|
||||
}
|
||||
@@ -292,7 +292,7 @@ pub const List = struct {
|
||||
frame.removeElementId(element, entry._value.str());
|
||||
}
|
||||
|
||||
element.asNode().bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
frame.attributeRemove(element, result.normalized, old_value);
|
||||
_ = frame._attribute_lookup.remove(@intFromPtr(entry));
|
||||
self._list.remove(&entry._node);
|
||||
|
||||
@@ -273,7 +273,7 @@ pub fn setInnerText(self: *HtmlElement, text: []const u8, frame: *Frame) !void {
|
||||
const parent = self.asElement().asNode();
|
||||
|
||||
// Remove all existing children
|
||||
parent.bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
var it = parent.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
frame.removeNode(parent, child, .{ .will_be_reconnected = false });
|
||||
|
||||
@@ -80,7 +80,7 @@ pub fn setSelected(self: *Option, selected: bool, frame: *Frame) !void {
|
||||
// TODO: When setting selected=true, may need to unselect other options
|
||||
// in the parent <select> if it doesn't have multiple attribute
|
||||
self._selected = selected;
|
||||
self.asElement().asNode().bumpDomVersion(frame);
|
||||
frame.domChanged();
|
||||
}
|
||||
|
||||
pub fn getDefaultSelected(self: *const Option) bool {
|
||||
|
||||
@@ -153,10 +153,14 @@ pub fn updateEntries(
|
||||
pub fn commitNavigation(self: *Navigation, frame: *Frame) !void {
|
||||
const url = frame.url;
|
||||
|
||||
const kind: NavigationKind = self._current_navigation_kind orelse .{ .push = null };
|
||||
var kind: NavigationKind = self._current_navigation_kind orelse .{ .push = null };
|
||||
defer self._current_navigation_kind = null;
|
||||
|
||||
const from_entry = self.getCurrentEntryOrNull();
|
||||
if (from_entry == null) {
|
||||
kind = .{ .push = null };
|
||||
}
|
||||
|
||||
try self.updateEntries(url, kind, frame, false);
|
||||
|
||||
self._activation = NavigationActivation{
|
||||
@@ -517,3 +521,31 @@ test "Navigation: about:blank commits entry" {
|
||||
);
|
||||
try testing.expect(result.isTrue());
|
||||
}
|
||||
|
||||
test "Navigation: reload on empty stack seeds an entry" {
|
||||
const frame = try testing.test_session.createPage();
|
||||
defer testing.test_session.removePage();
|
||||
|
||||
testing.test_session.navigation._entries.clearRetainingCapacity();
|
||||
testing.test_session.navigation._index = 0;
|
||||
|
||||
// Mirrors CDP Page.reload arriving on a fresh session: doReload reads
|
||||
// frame.url (default "about:blank") and routes through the synchronous
|
||||
// about:blank commit with kind=.reload. updateEntries is a no-op for
|
||||
// .reload, so without the empty-stack guard this would assert
|
||||
// `len: 0` in getCurrentEntry.
|
||||
try frame.navigate("about:blank", .{ .kind = .reload });
|
||||
|
||||
var runner = try testing.test_session.runner(.{});
|
||||
try runner.wait(.{ .ms = 1000 });
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
const result = try ls.local.exec(
|
||||
"navigation.currentEntry.url === 'about:blank'",
|
||||
"Navigation.test",
|
||||
);
|
||||
try testing.expect(result.isTrue());
|
||||
}
|
||||
|
||||
@@ -145,8 +145,8 @@ fn releaseSelfRef(self: *XMLHttpRequest) void {
|
||||
if (self._active_request == false) {
|
||||
return;
|
||||
}
|
||||
self.releaseRef(self._exec.context.page);
|
||||
self._active_request = false;
|
||||
self.releaseRef(self._exec.context.page);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *XMLHttpRequest, page: *Page) void {
|
||||
|
||||
6
src/network/cache/Cache.zig
vendored
6
src/network/cache/Cache.zig
vendored
@@ -49,6 +49,12 @@ pub fn put(self: *Cache, metadata: CachedMetadata, body: []const u8) !void {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clear(self: *Cache) !void {
|
||||
return switch (self.kind) {
|
||||
inline else => |*c| c.clear(),
|
||||
};
|
||||
}
|
||||
|
||||
pub const CacheControl = struct {
|
||||
max_age: u64,
|
||||
|
||||
|
||||
113
src/network/cache/FsCache.zig
vendored
113
src/network/cache/FsCache.zig
vendored
@@ -264,6 +264,22 @@ pub fn put(self: *FsCache, meta: CachedMetadata, body: []const u8) !void {
|
||||
log.debug(.cache, "put", .{ .url = meta.url, .hash = &hashed_key, .body_len = body.len });
|
||||
}
|
||||
|
||||
pub fn clear(self: *FsCache) !void {
|
||||
for (&self.locks) |*lock| lock.lock();
|
||||
defer for (&self.locks) |*lock| lock.unlock();
|
||||
|
||||
var iter = self.dir.iterate();
|
||||
while (try iter.next()) |entry| {
|
||||
if (entry.kind != .file) continue;
|
||||
if (!std.mem.endsWith(u8, entry.name, ".cache") and
|
||||
!std.mem.endsWith(u8, entry.name, ".cache.tmp")) continue;
|
||||
|
||||
self.dir.deleteFile(entry.name) catch |e| {
|
||||
log.err(.cache, "clear delete fail", .{ .file = entry.name, .err = e });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
fn setupCache() !struct { tmp: testing.TmpDir, cache: Cache } {
|
||||
@@ -617,3 +633,100 @@ test "FsCache: vary multiple headers" {
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
test "FsCache: clear removes all entries" {
|
||||
var setup = try setupCache();
|
||||
defer {
|
||||
setup.cache.deinit();
|
||||
setup.tmp.cleanup();
|
||||
}
|
||||
|
||||
const cache = &setup.cache;
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const now = std.time.timestamp();
|
||||
const base_meta_a = CachedMetadata{
|
||||
.url = "https://example.com/a",
|
||||
.status = 200,
|
||||
.stored_at = now,
|
||||
.age_at_store = 0,
|
||||
.cache_control = .{ .max_age = 600 },
|
||||
.headers = &.{},
|
||||
.vary_headers = &.{},
|
||||
.content_type = "text/html",
|
||||
};
|
||||
|
||||
const base_meta_b = CachedMetadata{
|
||||
.url = "https://example.com/b",
|
||||
.status = 200,
|
||||
.stored_at = now,
|
||||
.age_at_store = 0,
|
||||
.cache_control = .{ .max_age = 600 },
|
||||
.headers = &.{},
|
||||
.vary_headers = &.{},
|
||||
.content_type = "text/html",
|
||||
};
|
||||
|
||||
try cache.put(base_meta_a, "body a");
|
||||
try cache.put(base_meta_b, "body b");
|
||||
|
||||
// Sanity check: both are cached
|
||||
const r1 = cache.get(arena.allocator(), .{ .url = "https://example.com/a", .timestamp = now, .request_headers = &.{} });
|
||||
try testing.expect(r1 != null);
|
||||
r1.?.data.file.file.close();
|
||||
|
||||
const r2 = cache.get(arena.allocator(), .{ .url = "https://example.com/b", .timestamp = now, .request_headers = &.{} });
|
||||
try testing.expect(r2 != null);
|
||||
r2.?.data.file.file.close();
|
||||
|
||||
try cache.clear();
|
||||
|
||||
try testing.expectEqual(null, cache.get(arena.allocator(), .{ .url = "https://example.com/a", .timestamp = now, .request_headers = &.{} }));
|
||||
try testing.expectEqual(null, cache.get(arena.allocator(), .{ .url = "https://example.com/b", .timestamp = now, .request_headers = &.{} }));
|
||||
}
|
||||
|
||||
test "FsCache: put after clear works" {
|
||||
var setup = try setupCache();
|
||||
defer {
|
||||
setup.cache.deinit();
|
||||
setup.tmp.cleanup();
|
||||
}
|
||||
|
||||
const cache = &setup.cache;
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const now = std.time.timestamp();
|
||||
const meta = CachedMetadata{
|
||||
.url = "https://example.com",
|
||||
.content_type = "text/html",
|
||||
.status = 200,
|
||||
.stored_at = now,
|
||||
.age_at_store = 0,
|
||||
.cache_control = .{ .max_age = 600 },
|
||||
.headers = &.{},
|
||||
.vary_headers = &.{},
|
||||
};
|
||||
|
||||
try cache.put(meta, "before clear");
|
||||
try cache.clear();
|
||||
|
||||
// Should be a miss after clear
|
||||
try testing.expectEqual(null, cache.get(arena.allocator(), .{ .url = "https://example.com", .timestamp = now, .request_headers = &.{} }));
|
||||
|
||||
// Put again after clear — should work normally
|
||||
try cache.put(meta, "after clear");
|
||||
const result = cache.get(arena.allocator(), .{ .url = "https://example.com", .timestamp = now, .request_headers = &.{} }) orelse return error.CacheMiss;
|
||||
const f = result.data.file;
|
||||
defer f.file.close();
|
||||
|
||||
var buf: [64]u8 = undefined;
|
||||
var file_reader = f.file.reader(&buf);
|
||||
try file_reader.seekTo(f.offset);
|
||||
const read_buf = try file_reader.interface.readAlloc(testing.allocator, f.len);
|
||||
defer testing.allocator.free(read_buf);
|
||||
try testing.expectEqualStrings("after clear", read_buf);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user