Merge pull request #2942 from lightpanda-io/dom-pr-01-abort-signal

webapi: AbortSignal conformance fixes (dependent signals, null reason, timeout after detach)
This commit is contained in:
Karl Seguin
2026-07-14 20:07:03 +08:00
committed by GitHub
2 changed files with 53 additions and 11 deletions

View File

@@ -37,8 +37,8 @@ pub fn getSignal(self: *const AbortController) *AbortSignal {
return self._signal;
}
pub fn abort(self: *AbortController, reason_: ?js.Value.Global, exec: *const Execution) !void {
try self._signal.abort(if (reason_) |r| .{ .js_val = r } else null, exec);
pub fn abort(self: *AbortController, reason_: ?js.Value, exec: *const Execution) !void {
try self._signal.abort(try AbortSignal.reasonFromJs(reason_), exec);
}
pub const JsApi = struct {

View File

@@ -35,14 +35,18 @@ const Dependend = union(enum) {
signal: *AbortSignal,
model_context_tool: *ModelContextTool,
fn markAborted(self: Dependend, reason_: ?Reason, exec: *const Execution) !void {
// Returns false if the dependent was already aborted, in which case no
// abort event must be dispatched for it.
fn markAborted(self: Dependend, reason_: ?Reason, exec: *const Execution) !bool {
switch (self) {
.signal => |dep| {
if (dep._aborted) return;
if (dep._aborted) return false;
try dep.markAborted(reason_, exec);
return true;
},
.model_context_tool => |dep| {
try dep.markAborted(exec);
return true;
},
}
}
@@ -101,8 +105,9 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, exec: *const Execution) !void
// so we never need to recurse here.
var to_dispatch: std.ArrayList(Dependend) = .{};
for (self._dependents.items) |dep| {
try dep.markAborted(self._reason, exec);
try to_dispatch.append(exec.arena, dep);
if (try dep.markAborted(self._reason, exec)) {
try to_dispatch.append(exec.arena, dep);
}
}
try self.dispatchAbortEvent(exec);
@@ -123,7 +128,11 @@ fn markAborted(self: *AbortSignal, reason_: ?Reason, exec: *const Execution) !vo
.undefined => self._reason = reason,
}
} else {
self._reason = .{ .dom = DOMException.fromError(error.AbortError).? };
// Allocate the DOMException so the reason keeps a single JS identity:
// dependent signals must expose the very same DOMException instance.
const dom = try exec.arena.create(DOMException);
dom.* = DOMException.fromError(error.AbortError).?;
self._reason = .{ .dom = dom };
}
}
@@ -140,10 +149,21 @@ fn dispatchAbortEvent(self: *AbortSignal, exec: *const Execution) !void {
}
}
// Converts an abort(reason) JS argument to a Reason. Per spec, only a
// missing or undefined reason falls back to the default "AbortError"
// DOMException: an explicit null (or any other value) is kept as-is.
pub fn reasonFromJs(reason_: ?js.Value) !?Reason {
const reason = reason_ orelse return null;
if (reason.isUndefined()) {
return null;
}
return .{ .js_val = try reason.persist() };
}
// Static method to create an already-aborted signal
pub fn createAborted(reason_: ?js.Value.Global, exec: *const Execution) !*AbortSignal {
pub fn createAborted(reason_: ?js.Value, exec: *const Execution) !*AbortSignal {
const signal = try init(exec);
try signal.abort(if (reason_) |r| .{ .js_val = r } else null, exec);
try signal.abort(try reasonFromJs(reason_), exec);
return signal;
}
@@ -207,7 +227,7 @@ pub fn throwIfAborted(self: *const AbortSignal, exec: *const Execution) !ThrowIf
const Reason = union(enum) {
js_val: js.Value.Global,
dom: DOMException,
dom: *DOMException,
string: []const u8,
undefined: void,
};
@@ -218,11 +238,33 @@ const TimeoutCallback = struct {
fn run(ctx: *anyopaque) !?u32 {
const self: *TimeoutCallback = @ptrCast(@alignCast(ctx));
self.signal.abort(.{ .dom = DOMException.fromError(error.TimeoutError).? }, self.exec) catch |err| {
self.timeoutAbort() catch |err| {
log.warn(.app, "abort signal timeout", .{ .err = err });
};
return null;
}
fn timeoutAbort(self: *TimeoutCallback) !void {
// Per spec, the abort is queued as a global task on the signal's
// global: it must not run when the global's document is no longer
// fully active (e.g. the iframe that created the signal was detached).
switch (self.exec.js.global) {
.frame => |frame| {
var current: ?@TypeOf(frame) = frame;
while (current) |f| : (current = f.parent) {
const iframe = f.iframe orelse continue;
if (!iframe.asNode().isConnected()) {
return;
}
}
},
.worker => {},
}
const dom = try self.exec.arena.create(DOMException);
dom.* = DOMException.fromError(error.TimeoutError).?;
try self.signal.abort(.{ .dom = dom }, self.exec);
}
};
pub const JsApi = struct {