webapi: tighten DOM allocation lifetimes

Use the call arena only for the AbortSignal dependent list that must survive re-entrant abort handlers, and move scratch-only DOM reads to the local arena. Store BeforeUnloadEvent.returnValue in the event-owned arena so it remains valid without accumulating in the longer-lived frame arena.

These allocator changes bound temporary memory to the shortest safe lifetime while preserving existing Web API behavior. Add coverage for replacing BeforeUnloadEvent.returnValue.
This commit is contained in:
Francis Bouvier
2026-07-13 15:55:25 +02:00
parent 8702dd8b62
commit 750f1f004c
8 changed files with 23 additions and 10 deletions

View File

@@ -940,3 +940,16 @@
child.removeEventListener('click', listener);
}
</script>
<script id=beforeUnloadEventReturnValue>
{
const event = document.createEvent('BeforeUnloadEvent');
testing.expectEqual('', event.returnValue);
event.returnValue = 'first';
testing.expectEqual('first', event.returnValue);
event.returnValue = 'replacement';
testing.expectEqual('replacement', event.returnValue);
}
</script>

View File

@@ -106,7 +106,7 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, exec: *const Execution) !void
var to_dispatch: std.ArrayList(Dependend) = .{};
for (self._dependents.items) |dep| {
if (try dep.markAborted(self._reason, exec)) {
try to_dispatch.append(exec.arena, dep);
try to_dispatch.append(exec.call_arena, dep);
}
}

View File

@@ -216,7 +216,7 @@ pub fn getLastModified(self: *const Document, frame: *Frame) ![]const u8 {
if (localtime_r(&timestamp, &tm) == null) {
return error.InvalidArgument;
}
return std.fmt.allocPrint(frame.call_arena, "{d:0>2}/{d:0>2}/{d} {d:0>2}:{d:0>2}:{d:0>2}", .{
return std.fmt.allocPrint(frame.local_arena, "{d:0>2}/{d:0>2}/{d} {d:0>2}:{d:0>2}:{d:0>2}", .{
@as(u32, @intCast(tm.tm_mon + 1)),
@as(u32, @intCast(tm.tm_mday)),
tm.tm_year + 1900,

View File

@@ -60,7 +60,7 @@ pub fn getWholeText(self: *Text, frame: *Frame) ![]const u8 {
var current: ?*Node = first;
while (current) |cur| : (current = cur.nextSibling()) {
if (!isExclusiveTextNode(cur)) break;
try buf.appendSlice(frame.call_arena, cur._type.cdata._data.str());
try buf.appendSlice(frame.local_arena, cur._type.cdata._data.str());
}
return buf.items;
}

View File

@@ -256,7 +256,7 @@ pub const JsApi = struct {
};
var names: std.ArrayList([]const u8) = .{};
const arena = exec.call_arena;
const arena = exec.local_arena;
const len = self.length(frame);
for (0..len) |i| {

View File

@@ -141,9 +141,9 @@ pub const JsApi = struct {
// element's data-* attributes, in attribute order.
fn getNames(self: *DOMStringMap, exec: *const js.Execution) !js.Array {
var names: std.ArrayList([]const u8) = .empty;
for (try self._element._attributes.getNames(exec.call_arena)) |attr_name| {
const camel = (try kebabToCamel(exec.call_arena, attr_name)) orelse continue;
try names.append(exec.call_arena, camel);
for (try self._element._attributes.getNames(exec.local_arena)) |attr_name| {
const camel = (try kebabToCamel(exec.local_arena, attr_name)) orelse continue;
try names.append(exec.local_arena, camel);
}
var arr = exec.js.local.?.newArray(@intCast(names.items.len));

View File

@@ -380,7 +380,7 @@ pub fn getAccessKeyLabel(self: *HtmlElement, frame: *Frame) ![]const u8 {
if (codepoints != 1) {
return "";
}
return std.fmt.allocPrint(frame.call_arena, "Alt+{s}", .{value});
return std.fmt.allocPrint(frame.local_arena, "Alt+{s}", .{value});
}
pub fn getPopover(self: *HtmlElement) ?[]const u8 {

View File

@@ -71,8 +71,8 @@ pub fn getReturnValue(self: *const BeforeUnloadEvent) []const u8 {
return self._return_value;
}
pub fn setReturnValue(self: *BeforeUnloadEvent, value: []const u8, frame: *Frame) !void {
self._return_value = try frame.dupeString(value);
pub fn setReturnValue(self: *BeforeUnloadEvent, value: []const u8) !void {
self._return_value = try self._proto._arena.dupe(u8, value);
}
pub const JsApi = struct {