mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 15:13:28 +00:00
Merge pull request #821 from lightpanda-io/abort_controller
Abort controller
This commit is contained in:
@@ -30,39 +30,39 @@ pub const Console = struct {
|
||||
timers: 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) {
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
log.info(.console, "info", .{ .args = try serializeValues(values, page) });
|
||||
}
|
||||
|
||||
pub fn static_info(values: []JsObject, page: *Page) !void {
|
||||
return static_log(values, page);
|
||||
pub fn _info(values: []JsObject, page: *Page) !void {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
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 {
|
||||
const label = label_ orelse "default";
|
||||
@@ -134,7 +134,7 @@ pub const Console = struct {
|
||||
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()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -33,16 +33,26 @@ pub const EventTarget = struct {
|
||||
pub const Self = parser.EventTarget;
|
||||
pub const Exception = DOMException;
|
||||
|
||||
pub fn toInterface(et: *parser.EventTarget, page: *Page) !Union {
|
||||
// Not all targets are *parser.Nodes. page.zig emits a "load" event
|
||||
// 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
|
||||
// data into the *parser.EventTarget should we need this for other types.
|
||||
// For now, for the Window, which is a singleton, we can do this:
|
||||
pub fn toInterface(e: *parser.Event, et: *parser.EventTarget, page: *Page) !Union {
|
||||
// libdom assumes that all event targets are libdom nodes. They are not.
|
||||
|
||||
// The window is a common non-node target, but it's easy to handle as
|
||||
// its a singleton.
|
||||
if (@intFromPtr(et) == @intFromPtr(&page.window.base)) {
|
||||
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
|
||||
|
||||
@@ -54,7 +54,7 @@ pub const Event = struct {
|
||||
|
||||
pub fn toInterface(evt: *parser.Event) !Union {
|
||||
return switch (try parser.eventGetInternalType(evt)) {
|
||||
.event => .{ .Event = evt },
|
||||
.event, .abort_signal => .{ .Event = evt },
|
||||
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
|
||||
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @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 {
|
||||
const et = try parser.eventTarget(self);
|
||||
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 {
|
||||
const et = try parser.eventCurrentTarget(self);
|
||||
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 {
|
||||
|
||||
188
src/browser/html/AbortController.zig
Normal file
188
src/browser/html/AbortController.zig
Normal 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" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -39,4 +39,5 @@ pub const Interfaces = .{
|
||||
@import("DataSet.zig"),
|
||||
@import("screen.zig").Interfaces,
|
||||
@import("error_event.zig").ErrorEvent,
|
||||
@import("AbortController.zig").Interfaces,
|
||||
};
|
||||
|
||||
@@ -526,6 +526,7 @@ pub const EventType = enum(u8) {
|
||||
custom_event = 2,
|
||||
mouse_event = 3,
|
||||
error_event = 4,
|
||||
abort_signal = 5,
|
||||
};
|
||||
|
||||
pub const MutationEvent = c.dom_mutation_event;
|
||||
|
||||
@@ -29,6 +29,8 @@ const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const CALL_ARENA_RETAIN = 1024 * 16;
|
||||
const CONTEXT_ARENA_RETAIN = 1024 * 64;
|
||||
|
||||
const js = @This();
|
||||
|
||||
// Global, should only be initialized once.
|
||||
pub const Platform = struct {
|
||||
inner: v8.Platform,
|
||||
@@ -1272,6 +1274,11 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
||||
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
|
||||
// the src of the module to load.
|
||||
fn resolveModuleCallback(
|
||||
@@ -1821,6 +1828,8 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
||||
inner: v8.Value,
|
||||
js_context: *const JsContext,
|
||||
|
||||
const _EXCEPTION_ID_KLUDGE = true;
|
||||
|
||||
// the caller needs to deinit the string returned
|
||||
pub fn exception(self: Exception, allocator: Allocator) ![]const u8 {
|
||||
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_")) {
|
||||
generateProperty(Struct, name[4..], isolate, template_proto);
|
||||
} 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);
|
||||
}
|
||||
|
||||
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 function_template = v8.FunctionTemplate.initCallback(isolate, struct {
|
||||
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);
|
||||
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 {
|
||||
@@ -2318,6 +2327,10 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
||||
return value.js_obj.toValue();
|
||||
}
|
||||
|
||||
if (@hasDecl(T, "_EXCEPTION_ID_KLUDGE")) {
|
||||
return isolate.throwException(value.inner);
|
||||
}
|
||||
|
||||
if (s.is_tuple) {
|
||||
// return the tuple struct as an array
|
||||
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 {
|
||||
if (comptime isSelfReceiver(Struct, named_function) == false) {
|
||||
return self.function(Struct, named_function, info);
|
||||
}
|
||||
|
||||
const js_context = self.js_context;
|
||||
const func = @field(Struct, named_function.name);
|
||||
comptime assertSelfReceiver(Struct, named_function);
|
||||
|
||||
var args = try self.getArgs(Struct, named_function, 1, info);
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
_ = 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 params = @typeInfo(@TypeOf(func)).@"fn".params;
|
||||
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.?;
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user