Merge pull request #821 from lightpanda-io/abort_controller

Abort controller
This commit is contained in:
Karl Seguin
2025-06-28 09:00:07 +08:00
committed by GitHub
7 changed files with 260 additions and 27 deletions

View File

@@ -30,39 +30,39 @@ pub const Console = struct {
timers: std.StringHashMapUnmanaged(u32) = .{}, timers: std.StringHashMapUnmanaged(u32) = .{},
counts: std.StringHashMapUnmanaged(u32) = .{}, counts: std.StringHashMapUnmanaged(u32) = .{},
pub fn static_lp(values: []JsObject, page: *Page) !void { pub fn _lp(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) }); log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) });
} }
pub fn static_log(values: []JsObject, page: *Page) !void { pub fn _log(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
log.info(.console, "info", .{ .args = try serializeValues(values, page) }); log.info(.console, "info", .{ .args = try serializeValues(values, page) });
} }
pub fn static_info(values: []JsObject, page: *Page) !void { pub fn _info(values: []JsObject, page: *Page) !void {
return static_log(values, page); return _log(values, page);
} }
pub fn static_debug(values: []JsObject, page: *Page) !void { pub fn _debug(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
log.debug(.console, "debug", .{ .args = try serializeValues(values, page) }); log.debug(.console, "debug", .{ .args = try serializeValues(values, page) });
} }
pub fn static_warn(values: []JsObject, page: *Page) !void { pub fn _warn(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
log.warn(.console, "warn", .{ .args = try serializeValues(values, page) }); log.warn(.console, "warn", .{ .args = try serializeValues(values, page) });
} }
pub fn static_error(values: []JsObject, page: *Page) !void { pub fn _error(values: []JsObject, page: *Page) !void {
if (values.len == 0) { if (values.len == 0) {
return; return;
} }
@@ -73,7 +73,7 @@ pub const Console = struct {
}); });
} }
pub fn static_clear() void {} pub fn _clear() void {}
pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void { pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void {
const label = label_ orelse "default"; const label = label_ orelse "default";
@@ -134,7 +134,7 @@ pub const Console = struct {
log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value }); log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value });
} }
pub fn static_assert(assertion: JsObject, values: []JsObject, page: *Page) !void { pub fn _assert(assertion: JsObject, values: []JsObject, page: *Page) !void {
if (assertion.isTruthy()) { if (assertion.isTruthy()) {
return; return;
} }

View File

@@ -33,16 +33,26 @@ pub const EventTarget = struct {
pub const Self = parser.EventTarget; pub const Self = parser.EventTarget;
pub const Exception = DOMException; pub const Exception = DOMException;
pub fn toInterface(et: *parser.EventTarget, page: *Page) !Union { pub fn toInterface(e: *parser.Event, et: *parser.EventTarget, page: *Page) !Union {
// Not all targets are *parser.Nodes. page.zig emits a "load" event // libdom assumes that all event targets are libdom nodes. They are not.
// where the target is a Window, which cannot be cast directly to a node.
// Ideally, we'd remove this duality. Failing that, we'll need to embed // The window is a common non-node target, but it's easy to handle as
// data into the *parser.EventTarget should we need this for other types. // its a singleton.
// For now, for the Window, which is a singleton, we can do this:
if (@intFromPtr(et) == @intFromPtr(&page.window.base)) { if (@intFromPtr(et) == @intFromPtr(&page.window.base)) {
return .{ .Window = &page.window }; return .{ .Window = &page.window };
} }
return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et)));
// AbortSignal is another non-node target. It has a distinct usage though
// so we hijack the event internal type to identity if.
switch (try parser.eventGetInternalType(e)) {
.abort_signal => {
return .{ .AbortSignal = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
},
else => {
// some of these probably need to be special-cased like abort_signal
return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et)));
},
}
} }
// JS funcs // JS funcs

View File

@@ -54,7 +54,7 @@ pub const Event = struct {
pub fn toInterface(evt: *parser.Event) !Union { pub fn toInterface(evt: *parser.Event) !Union {
return switch (try parser.eventGetInternalType(evt)) { return switch (try parser.eventGetInternalType(evt)) {
.event => .{ .Event = evt }, .event, .abort_signal => .{ .Event = evt },
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* }, .custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* }, .progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) }, .mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
@@ -77,13 +77,13 @@ pub const Event = struct {
pub fn get_target(self: *parser.Event, page: *Page) !?EventTargetUnion { pub fn get_target(self: *parser.Event, page: *Page) !?EventTargetUnion {
const et = try parser.eventTarget(self); const et = try parser.eventTarget(self);
if (et == null) return null; if (et == null) return null;
return try EventTarget.toInterface(et.?, page); return try EventTarget.toInterface(self, et.?, page);
} }
pub fn get_currentTarget(self: *parser.Event, page: *Page) !?EventTargetUnion { pub fn get_currentTarget(self: *parser.Event, page: *Page) !?EventTargetUnion {
const et = try parser.eventCurrentTarget(self); const et = try parser.eventCurrentTarget(self);
if (et == null) return null; if (et == null) return null;
return try EventTarget.toInterface(et.?, page); return try EventTarget.toInterface(self, et.?, page);
} }
pub fn get_eventPhase(self: *parser.Event) !u8 { pub fn get_eventPhase(self: *parser.Event) !u8 {

View File

@@ -0,0 +1,188 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const Loop = @import("../../runtime/loop.zig").Loop;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
pub const Interfaces = .{
AbortController,
AbortSignal,
};
const AbortController = @This();
signal: *AbortSignal,
pub fn constructor(page: *Page) !AbortController {
// Why do we allocate this rather than storing directly in the struct?
// https://github.com/lightpanda-io/project/discussions/165
const signal = try page.arena.create(AbortSignal);
signal.* = .init;
return .{
.signal = signal,
};
}
pub fn get_signal(self: *AbortController) *AbortSignal {
return self.signal;
}
pub fn _abort(self: *AbortController, reason_: ?[]const u8) !void {
return self.signal.abort(reason_);
}
pub const AbortSignal = struct {
const DEFAULT_REASON = "AbortError";
pub const prototype = *EventTarget;
proto: parser.EventTargetTBase = .{},
aborted: bool,
reason: ?[]const u8,
pub const init: AbortSignal = .{
.proto = .{},
.reason = null,
.aborted = false,
};
pub fn static_abort(reason_: ?[]const u8) AbortSignal {
return .{
.aborted = true,
.reason = reason_ orelse DEFAULT_REASON,
};
}
pub fn static_timeout(delay: u32, page: *Page) !*AbortSignal {
const callback = try page.arena.create(TimeoutCallback);
callback.* = .{
.signal = .init,
.node = .{ .func = TimeoutCallback.run },
};
const delay_ms: u63 = @as(u63, delay) * std.time.ns_per_ms;
_ = try page.loop.timeout(delay_ms, &callback.node);
return &callback.signal;
}
pub fn get_aborted(self: *const AbortSignal) bool {
return self.aborted;
}
fn abort(self: *AbortSignal, reason_: ?[]const u8) !void {
self.aborted = true;
self.reason = reason_ orelse DEFAULT_REASON;
const abort_event = try parser.eventCreate();
try parser.eventSetInternalType(abort_event, .abort_signal);
defer parser.eventDestroy(abort_event);
try parser.eventInit(abort_event, "abort", .{});
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(AbortSignal, self),
abort_event,
);
}
const Reason = union(enum) {
reason: []const u8,
undefined: void,
};
pub fn get_reason(self: *const AbortSignal) Reason {
if (self.reason) |r| {
return .{ .reason = r };
}
return .{ .undefined = {} };
}
const ThrowIfAborted = union(enum) {
exception: Env.Exception,
undefined: void,
};
pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted {
if (self.aborted) {
const ex = page.main_context.throw(self.reason orelse DEFAULT_REASON);
return .{ .exception = ex };
}
return .{ .undefined = {} };
}
};
const TimeoutCallback = struct {
signal: AbortSignal,
// This is the internal data that the event loop tracks. We'll get this
// back in run and, from it, can get our TimeoutCallback instance
node: Loop.CallbackNode = undefined,
fn run(node: *Loop.CallbackNode, _: *?u63) void {
const self: *TimeoutCallback = @fieldParentPtr("node", node);
self.signal.abort("TimeoutError") catch |err| {
log.warn(.app, "abort signal timeout", .{ .err = err });
};
}
};
const testing = @import("../../testing.zig");
test "Browser.HTML.AbortController" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "var called = 0", null },
.{ "var a1 = new AbortController()", null },
.{ "var s1 = a1.signal", null },
.{ "s1.throwIfAborted()", "undefined" },
.{ "s1.reason", "undefined" },
.{ "var target;", null },
.{
\\ s1.addEventListener('abort', (e) => {
\\ called += 1;
\\ target = e.target;
\\
\\ });
,
null,
},
.{ "a1.abort()", null },
.{ "s1.aborted", "true" },
.{ "target == s1", "true" },
.{ "s1.reason", "AbortError" },
.{ "called", "1" },
}, .{});
try runner.testCases(&.{
.{ "var s2 = AbortSignal.abort('over 9000')", null },
.{ "s2.aborted", "true" },
.{ "s2.reason", "over 9000" },
.{ "AbortSignal.abort().reason", "AbortError" },
}, .{});
try runner.testCases(&.{
.{ "var s3 = AbortSignal.timeout(10)", null },
.{ "s3.aborted", "true" },
.{ "s3.reason", "TimeoutError" },
.{ "try { s3.throwIfAborted() } catch (e) { e }", "Error: TimeoutError" },
}, .{});
}

View File

@@ -39,4 +39,5 @@ pub const Interfaces = .{
@import("DataSet.zig"), @import("DataSet.zig"),
@import("screen.zig").Interfaces, @import("screen.zig").Interfaces,
@import("error_event.zig").ErrorEvent, @import("error_event.zig").ErrorEvent,
@import("AbortController.zig").Interfaces,
}; };

View File

@@ -526,6 +526,7 @@ pub const EventType = enum(u8) {
custom_event = 2, custom_event = 2,
mouse_event = 3, mouse_event = 3,
error_event = 4, error_event = 4,
abort_signal = 5,
}; };
pub const MutationEvent = c.dom_mutation_event; pub const MutationEvent = c.dom_mutation_event;

View File

@@ -29,6 +29,8 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const CALL_ARENA_RETAIN = 1024 * 16; const CALL_ARENA_RETAIN = 1024 * 16;
const CONTEXT_ARENA_RETAIN = 1024 * 64; const CONTEXT_ARENA_RETAIN = 1024 * 64;
const js = @This();
// Global, should only be initialized once. // Global, should only be initialized once.
pub const Platform = struct { pub const Platform = struct {
inner: v8.Platform, inner: v8.Platform,
@@ -1272,6 +1274,11 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
return .{ .invalid = {} }; return .{ .invalid = {} };
} }
pub fn throw(self: *JsContext, err: []const u8) Exception {
const js_value = js.createException(self.isolate, err);
return self.createException(js_value);
}
// Callback from V8, asking us to load a module. The "specifier" is // Callback from V8, asking us to load a module. The "specifier" is
// the src of the module to load. // the src of the module to load.
fn resolveModuleCallback( fn resolveModuleCallback(
@@ -1821,6 +1828,8 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
inner: v8.Value, inner: v8.Value,
js_context: *const JsContext, js_context: *const JsContext,
const _EXCEPTION_ID_KLUDGE = true;
// the caller needs to deinit the string returned // the caller needs to deinit the string returned
pub fn exception(self: Exception, allocator: Allocator) ![]const u8 { pub fn exception(self: Exception, allocator: Allocator) ![]const u8 {
const js_context = self.js_context; const js_context = self.js_context;
@@ -1919,7 +1928,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
} else if (comptime std.mem.startsWith(u8, name, "get_")) { } else if (comptime std.mem.startsWith(u8, name, "get_")) {
generateProperty(Struct, name[4..], isolate, template_proto); generateProperty(Struct, name[4..], isolate, template_proto);
} else if (comptime std.mem.startsWith(u8, name, "static_")) { } else if (comptime std.mem.startsWith(u8, name, "static_")) {
generateFunction(Struct, name[7..], isolate, template_proto); generateFunction(Struct, name[7..], isolate, template);
} }
} }
@@ -2009,7 +2018,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
template_proto.set(js_name, function_template, v8.PropertyAttribute.None); template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
} }
fn generateFunction(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template_proto: v8.ObjectTemplate) void { fn generateFunction(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template: v8.FunctionTemplate) void {
const js_name = v8.String.initUtf8(isolate, name).toName(); const js_name = v8.String.initUtf8(isolate, name).toName();
const function_template = v8.FunctionTemplate.initCallback(isolate, struct { const function_template = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
@@ -2023,7 +2032,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
}; };
} }
}.callback); }.callback);
template_proto.set(js_name, function_template, v8.PropertyAttribute.None); template.set(js_name, function_template, v8.PropertyAttribute.None);
} }
fn generateAttribute(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template: v8.FunctionTemplate, template_proto: v8.ObjectTemplate) void { fn generateAttribute(comptime Struct: type, comptime name: []const u8, isolate: v8.Isolate, template: v8.FunctionTemplate, template_proto: v8.ObjectTemplate) void {
@@ -2318,6 +2327,10 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
return value.js_obj.toValue(); return value.js_obj.toValue();
} }
if (@hasDecl(T, "_EXCEPTION_ID_KLUDGE")) {
return isolate.throwException(value.inner);
}
if (s.is_tuple) { if (s.is_tuple) {
// return the tuple struct as an array // return the tuple struct as an array
var js_arr = v8.Array.init(isolate, @intCast(s.fields.len)); var js_arr = v8.Array.init(isolate, @intCast(s.fields.len));
@@ -2619,10 +2632,12 @@ fn Caller(comptime E: type, comptime State: type) type {
} }
fn method(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void { fn method(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
if (comptime isSelfReceiver(Struct, named_function) == false) {
return self.function(Struct, named_function, info);
}
const js_context = self.js_context; const js_context = self.js_context;
const func = @field(Struct, named_function.name); const func = @field(Struct, named_function.name);
comptime assertSelfReceiver(Struct, named_function);
var args = try self.getArgs(Struct, named_function, 1, info); var args = try self.getArgs(Struct, named_function, 1, info);
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis()); const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
@@ -2742,18 +2757,36 @@ fn Caller(comptime E: type, comptime State: type) type {
return valueToString(self.call_arena, .{ .handle = name.handle }, self.isolate, self.v8_context); return valueToString(self.call_arena, .{ .handle = name.handle }, self.isolate, self.v8_context);
} }
fn isSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction) bool {
return checkSelfReceiver(Struct, named_function, false);
}
fn assertSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction) void { fn assertSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction) void {
_ = checkSelfReceiver(Struct, named_function, true);
}
fn checkSelfReceiver(comptime Struct: type, comptime named_function: NamedFunction, comptime fail: bool) bool {
const func = @field(Struct, named_function.name); const func = @field(Struct, named_function.name);
const params = @typeInfo(@TypeOf(func)).@"fn".params; const params = @typeInfo(@TypeOf(func)).@"fn".params;
if (params.len == 0) { if (params.len == 0) {
@compileError(named_function.full_name ++ " must have a self parameter"); if (fail) {
@compileError(named_function.full_name ++ " must have a self parameter");
}
return false;
} }
const R = Receiver(Struct);
const R = Receiver(Struct);
const first_param = params[0].type.?; const first_param = params[0].type.?;
if (first_param != *R and first_param != *const R) { if (first_param != *R and first_param != *const R) {
@compileError(std.fmt.comptimePrint("The first parameter to {s} must be a *{s} or *const {s}. Got: {s}", .{ named_function.full_name, @typeName(R), @typeName(R), @typeName(first_param) })); if (fail) {
@compileError(std.fmt.comptimePrint("The first parameter to {s} must be a *{s} or *const {s}. Got: {s}", .{
named_function.full_name,
@typeName(R),
@typeName(R),
@typeName(first_param),
}));
}
return false;
} }
return true;
} }
fn assertIsStateArg(comptime Struct: type, comptime named_function: NamedFunction, index: comptime_int) void { fn assertIsStateArg(comptime Struct: type, comptime named_function: NamedFunction, index: comptime_int) void {