Skip to content

Commit bbd9e5e

Browse files
committed
add AbortController API
1 parent 08d2ea6 commit bbd9e5e

File tree

6 files changed

+180
-60
lines changed

6 files changed

+180
-60
lines changed

src/browser/console/console.zig

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,39 +30,39 @@ pub const Console = struct {
3030
timers: std.StringHashMapUnmanaged(u32) = .{},
3131
counts: std.StringHashMapUnmanaged(u32) = .{},
3232

33-
pub fn static_lp(values: []JsObject, page: *Page) !void {
33+
pub fn _lp(values: []JsObject, page: *Page) !void {
3434
if (values.len == 0) {
3535
return;
3636
}
3737
log.fatal(.console, "lightpanda", .{ .args = try serializeValues(values, page) });
3838
}
3939

40-
pub fn static_log(values: []JsObject, page: *Page) !void {
40+
pub fn _log(values: []JsObject, page: *Page) !void {
4141
if (values.len == 0) {
4242
return;
4343
}
4444
log.info(.console, "info", .{ .args = try serializeValues(values, page) });
4545
}
4646

47-
pub fn static_info(values: []JsObject, page: *Page) !void {
48-
return static_log(values, page);
47+
pub fn _info(values: []JsObject, page: *Page) !void {
48+
return _log(values, page);
4949
}
5050

51-
pub fn static_debug(values: []JsObject, page: *Page) !void {
51+
pub fn _debug(values: []JsObject, page: *Page) !void {
5252
if (values.len == 0) {
5353
return;
5454
}
5555
log.debug(.console, "debug", .{ .args = try serializeValues(values, page) });
5656
}
5757

58-
pub fn static_warn(values: []JsObject, page: *Page) !void {
58+
pub fn _warn(values: []JsObject, page: *Page) !void {
5959
if (values.len == 0) {
6060
return;
6161
}
6262
log.warn(.console, "warn", .{ .args = try serializeValues(values, page) });
6363
}
6464

65-
pub fn static_error(values: []JsObject, page: *Page) !void {
65+
pub fn _error(values: []JsObject, page: *Page) !void {
6666
if (values.len == 0) {
6767
return;
6868
}
@@ -73,7 +73,7 @@ pub const Console = struct {
7373
});
7474
}
7575

76-
pub fn static_clear() void {}
76+
pub fn _clear() void {}
7777

7878
pub fn _count(self: *Console, label_: ?[]const u8, page: *Page) !void {
7979
const label = label_ orelse "default";
@@ -134,7 +134,7 @@ pub const Console = struct {
134134
log.warn(.console, "timer stop", .{ .label = label, .elapsed = elapsed - kv.value });
135135
}
136136

137-
pub fn static_assert(assertion: JsObject, values: []JsObject, page: *Page) !void {
137+
pub fn _assert(assertion: JsObject, values: []JsObject, page: *Page) !void {
138138
if (assertion.isTruthy()) {
139139
return;
140140
}

src/browser/dom/event_target.zig

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,26 @@ pub const EventTarget = struct {
3333
pub const Self = parser.EventTarget;
3434
pub const Exception = DOMException;
3535

36-
pub fn toInterface(et: *parser.EventTarget, page: *Page) !Union {
37-
// Not all targets are *parser.Nodes. page.zig emits a "load" event
38-
// where the target is a Window, which cannot be cast directly to a node.
39-
// Ideally, we'd remove this duality. Failing that, we'll need to embed
40-
// data into the *parser.EventTarget should we need this for other types.
41-
// For now, for the Window, which is a singleton, we can do this:
36+
pub fn toInterface(e: *parser.Event, et: *parser.EventTarget, page: *Page) !Union {
37+
// libdom assumes that all event targets are libdom nodes. They are not.
38+
39+
// The window is a common non-node target, but it's easy to handle as
40+
// its a singleton.
4241
if (@intFromPtr(et) == @intFromPtr(&page.window.base)) {
4342
return .{ .Window = &page.window };
4443
}
45-
return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et)));
44+
45+
// AbortSignal is another non-node target. It has a distinct usage though
46+
// so we hijack the event internal type to identity if.
47+
switch (try parser.eventGetInternalType(e)) {
48+
.abort_signal => {
49+
return .{ .AbortSignal = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
50+
},
51+
else => {
52+
// some of these probably need to be special-cased like abort_signal
53+
return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et)));
54+
},
55+
}
4656
}
4757

4858
// JS funcs

src/browser/events/event.zig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub const Event = struct {
5454

5555
pub fn toInterface(evt: *parser.Event) !Union {
5656
return switch (try parser.eventGetInternalType(evt)) {
57-
.event => .{ .Event = evt },
57+
.event, .abort_signal => .{ .Event = evt },
5858
.custom_event => .{ .CustomEvent = @as(*CustomEvent, @ptrCast(evt)).* },
5959
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
6060
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
@@ -77,13 +77,13 @@ pub const Event = struct {
7777
pub fn get_target(self: *parser.Event, page: *Page) !?EventTargetUnion {
7878
const et = try parser.eventTarget(self);
7979
if (et == null) return null;
80-
return try EventTarget.toInterface(et.?, page);
80+
return try EventTarget.toInterface(self, et.?, page);
8181
}
8282

8383
pub fn get_currentTarget(self: *parser.Event, page: *Page) !?EventTargetUnion {
8484
const et = try parser.eventCurrentTarget(self);
8585
if (et == null) return null;
86-
return try EventTarget.toInterface(et.?, page);
86+
return try EventTarget.toInterface(self, et.?, page);
8787
}
8888

8989
pub fn get_eventPhase(self: *parser.Event) !u8 {

src/browser/html/AbortController.zig

Lines changed: 109 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,72 +17,131 @@
1717
// along with this program. If not, see <https://www.gnu.org/licenses/>.
1818

1919
const std = @import("std");
20+
const log = @import("../../log.zig");
2021
const parser = @import("../netsurf.zig");
22+
const Env = @import("../env.zig").Env;
23+
const Page = @import("../page.zig").Page;
24+
const Loop = @import("../../runtime/loop.zig").Loop;
2125
const EventTarget = @import("../dom/event_target.zig").EventTarget;
2226

2327
pub const Interfaces = .{
2428
AbortController,
25-
Signal,
29+
AbortSignal,
2630
};
2731

2832
const AbortController = @This();
2933

30-
signal: ?Signal = null,
34+
signal: *AbortSignal,
3135

32-
pub fn constructor() AbortController {
33-
return .{};
34-
}
36+
pub fn constructor(page: *Page) !AbortController {
37+
// Why do we allocate this rather than storing directly in the struct?
38+
// https://github.com/lightpanda-io/project/discussions/165
39+
const signal = try page.arena.create(AbortSignal);
40+
signal.* = .init;
3541

36-
pub fn get_signal(self: *AbortController) *Signal {
37-
if (self.signal) |*s| {
38-
return s;
39-
}
40-
self.signal = .init;
41-
return &self.signal.?;
42+
return .{
43+
.signal = signal,
44+
};
4245
}
4346

44-
pub fn abort(self: *AbortController, reason_: ?[]const u8) void {
45-
const signal = &self.signal;
46-
47-
signal.aborted = true;
48-
signal.reason = reason_ orelse "AbortError";
47+
pub fn get_signal(self: *AbortController) *AbortSignal {
48+
return self.signal;
49+
}
4950

50-
const abort_event = try parser.eventCreate();
51-
defer parser.eventDestroy(abort_event);
52-
try parser.eventInit(abort_event, "abort", .{});
53-
_ = try parser.eventTargetDispatchEvent(
54-
parser.toEventTarget(Signal, signal),
55-
abort_event,
56-
);
51+
pub fn _abort(self: *AbortController, reason_: ?[]const u8) !void {
52+
return self.signal.abort(reason_);
5753
}
5854

59-
pub const Signal = struct {
55+
pub const AbortSignal = struct {
56+
const DEFAULT_REASON = "AbortError";
57+
6058
pub const prototype = *EventTarget;
59+
proto: parser.EventTargetTBase = .{},
6160

6261
aborted: bool,
6362
reason: ?[]const u8,
64-
proto: parser.EventTargetTBase,
6563

66-
pub const init: Signal = .{
64+
pub const init: AbortSignal = .{
6765
.proto = .{},
6866
.reason = null,
6967
.aborted = false,
7068
};
7169

72-
pub fn get_aborted(self: *const Signal) bool {
70+
pub fn static_abort(reason_: ?[]const u8) AbortSignal {
71+
return .{
72+
.aborted = true,
73+
.reason = reason_ orelse DEFAULT_REASON,
74+
};
75+
}
76+
77+
pub fn static_timeout(delay: u32, page: *Page) !*AbortSignal {
78+
const callback = try page.arena.create(TimeoutCallback);
79+
callback.* = .{
80+
.signal = .init,
81+
.node = .{ .func = TimeoutCallback.run },
82+
};
83+
84+
const delay_ms: u63 = @as(u63, delay) * std.time.ns_per_ms;
85+
_ = try page.loop.timeout(delay_ms, &callback.node);
86+
return &callback.signal;
87+
}
88+
89+
pub fn get_aborted(self: *const AbortSignal) bool {
7390
return self.aborted;
7491
}
7592

93+
fn abort(self: *AbortSignal, reason_: ?[]const u8) !void {
94+
self.aborted = true;
95+
self.reason = reason_ orelse DEFAULT_REASON;
96+
97+
const abort_event = try parser.eventCreate();
98+
try parser.eventSetInternalType(abort_event, .abort_signal);
99+
100+
defer parser.eventDestroy(abort_event);
101+
try parser.eventInit(abort_event, "abort", .{});
102+
_ = try parser.eventTargetDispatchEvent(
103+
parser.toEventTarget(AbortSignal, self),
104+
abort_event,
105+
);
106+
}
107+
76108
const Reason = union(enum) {
77109
reason: []const u8,
78110
undefined: void,
79111
};
80-
pub fn get_reason(self: *const Signal) Reason {
112+
pub fn get_reason(self: *const AbortSignal) Reason {
81113
if (self.reason) |r| {
82114
return .{ .reason = r };
83115
}
84116
return .{ .undefined = {} };
85117
}
118+
119+
const ThrowIfAborted = union(enum) {
120+
exception: Env.Exception,
121+
undefined: void,
122+
};
123+
pub fn _throwIfAborted(self: *const AbortSignal, page: *Page) ThrowIfAborted {
124+
if (self.aborted) {
125+
const ex = page.main_context.throw(self.reason orelse DEFAULT_REASON);
126+
return .{ .exception = ex };
127+
}
128+
return .{ .undefined = {} };
129+
}
130+
};
131+
132+
const TimeoutCallback = struct {
133+
signal: AbortSignal,
134+
135+
// This is the internal data that the event loop tracks. We'll get this
136+
// back in run and, from it, can get our TimeoutCallback instance
137+
node: Loop.CallbackNode = undefined,
138+
139+
fn run(node: *Loop.CallbackNode, _: *?u63) void {
140+
const self: *TimeoutCallback = @fieldParentPtr("node", node);
141+
self.signal.abort("TimeoutError") catch |err| {
142+
log.warn(.app, "abort signal timeout", .{ .err = err });
143+
};
144+
}
86145
};
87146

88147
const testing = @import("../../testing.zig");
@@ -91,22 +150,39 @@ test "Browser.HTML.AbortController" {
91150
defer runner.deinit();
92151

93152
try runner.testCases(&.{
94-
.{ "var called = false", null },
153+
.{ "var called = 0", null },
95154
.{ "var a1 = new AbortController()", null },
96155
.{ "var s1 = a1.signal", null },
156+
.{ "s1.throwIfAborted()", "undefined" },
97157
.{ "s1.reason", "undefined" },
98158
.{ "var target;", null },
99159
.{
100160
\\ s1.addEventListener('abort', (e) => {
101-
\\ called = 1;
161+
\\ called += 1;
102162
\\ target = e.target;
103163
\\
104164
\\ });
105-
\\ target == s1
106-
, "true" },
165+
,
166+
null,
167+
},
107168
.{ "a1.abort()", null },
108169
.{ "s1.aborted", "true" },
109-
.{ "s1.reason", "undefined" },
170+
.{ "target == s1", "true" },
171+
.{ "s1.reason", "AbortError" },
110172
.{ "called", "1" },
111173
}, .{});
174+
175+
try runner.testCases(&.{
176+
.{ "var s2 = AbortSignal.abort('over 9000')", null },
177+
.{ "s2.aborted", "true" },
178+
.{ "s2.reason", "over 9000" },
179+
.{ "AbortSignal.abort().reason", "AbortError" },
180+
}, .{});
181+
182+
try runner.testCases(&.{
183+
.{ "var s3 = AbortSignal.timeout(10)", null },
184+
.{ "s3.aborted", "true" },
185+
.{ "s3.reason", "TimeoutError" },
186+
.{ "try { s3.throwIfAborted() } catch (e) { e }", "Error: TimeoutError" },
187+
}, .{});
112188
}

src/browser/netsurf.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,7 @@ pub const EventType = enum(u8) {
526526
custom_event = 2,
527527
mouse_event = 3,
528528
error_event = 4,
529+
abort_signal = 5,
529530
};
530531

531532
pub const MutationEvent = c.dom_mutation_event;

0 commit comments

Comments
 (0)