Add Finalizers to events

At a high level, this does for Events what was recently done for XHR, Fetch and
Observers. Events are self-contained in their own arena from the ArenaPool and
are registered with v8 to be finalized.

But events are more complicated than those other types. For one, events have
a prototype chain. (XHR also does, but it's always the top-level object that's
created, whereas it's valid to create a base Event or something that inherits
from Event). But the _real_ complication is that Events, unlike previous types,
can be created from Zig or from V8.

This is something that Fetch had to deal with too, because the Response is only
given to V8 on success. So in Fetch, there's a period of time where Zig is
solely responsible for the Response, until it's passed to v8. But with events
it's a lot more subtle.

There are 3 possibilities:
1 - An Event is created from v8. This is the simplest, and it simply becomes a
    a weak reference for us. When v8 is done with it, the finalizer is called.
2 - An Event is created in Zig (e.g. window.load) and dispatched to v8. Again
    we can rely on the v8 finalizer.
3 - An event is created in Zig, but not dispatched to v8 (e.g. there are no
    listeners), Zig has to release the event.

(It's worth pointing out that one thing that still keeps this relatively
straightforward is that we never hold on to Events past some pretty clear point)

Now, it would seem that #3 is the only issue we have to deal with, and maybe
we can do something like:

```
if (event_manager.hasListener("load", capture)) {
   try event_manager.dispatch(event);
} else {
   event.deinit();
}
```

In fact, in many cases, we could use this to optimize not even creating the
event:

```
if (event_manager.hasListener("load, capture)) {
   const event = try createEvent("load", capture);
   try event_manager.dispatch(event);
}
```

And that's an optimization worth considering, but it isn't good enough to
properly manage memory. Do you see the issue? There could be a listener (so we
think v8 owns it), but we might never give the value to v8. Any failure between
hasListener and actually handing the value to v8 would result in a leak.

To solve this, the bridge will now set a _v8_handover flag (if present) once it
has created the finalizer_callback entry. So dispatching code now becomes:

```
const event = try createEvent("load", capture);
defer if (!event._v8_handover) event.deinit(false);
try event_manager.dispatch(event);
```

The v8 finalizer callback was also improved. Previously, we just embedded the
pointer to the zig object. In the v8 callback, we could cast that back to T
and call deinit. But, because of possible timing issues between when (if) v8
calls the finalizer, and our own cleanup, the code would check in the context to
see if the ptr was still valid. Wait, what? We're using the ptr to get the
context to see if the ptr is valid?

We now store a pointer to the FinalizerCallback which contains the context.
So instead of something stupid like:

```
// note, if the identity_map doesn't contain the value, then value is likely
// invalid, and value.page will segfault
value.page.js.identity_map.contains(@intFromPtr(value))
```

We do:
```
if (fc.ctx.finalizer_callbacks.contains(@intFromPtr(fc.value)) {
  // fc.value is safe to use
}
```
This commit is contained in:
Karl Seguin
2026-02-06 18:28:12 +08:00
parent 82f9e70406
commit c4e82407ec
31 changed files with 445 additions and 197 deletions

View File

@@ -172,60 +172,42 @@ pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
return chain.get(1);
}
fn eventInit(typ: []const u8, value: anytype, page: *Page) !Event {
// Round to 2ms for privacy (browsers do this)
const raw_timestamp = @import("../datetime.zig").milliTimestamp(.monotonic);
const time_stamp = (raw_timestamp / 2) * 2;
return .{
._type = unionInit(Event.Type, value),
._type_string = try String.init(page.arena, typ, .{}),
._time_stamp = time_stamp,
};
}
// this is a root object
pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator();
pub fn event(self: *Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
const chain = try PrototypeChain(
&.{ Event, @TypeOf(child) },
).allocate(allocator);
).allocate(arena);
// Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0);
event_ptr.* = try eventInit(typ, chain.get(1), self._page);
event_ptr.* = try self.eventInit(arena, typ, chain.get(1));
chain.setLeaf(1, child);
return chain.get(1);
}
pub fn uiEvent(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator();
pub fn uiEvent(self: *Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
const chain = try PrototypeChain(
&.{ Event, UIEvent, @TypeOf(child) },
).allocate(allocator);
).allocate(arena);
// Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0);
event_ptr.* = try eventInit(typ, chain.get(1), self._page);
event_ptr.* = try self.eventInit(arena, typ, chain.get(1));
chain.setMiddle(1, UIEvent.Type);
chain.setLeaf(2, child);
return chain.get(2);
}
pub fn mouseEvent(self: *Factory, typ: []const u8, mouse: MouseEvent, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator();
pub fn mouseEvent(self: *Factory, arena: Allocator, typ: String, mouse: MouseEvent, child: anytype) !*@TypeOf(child) {
const chain = try PrototypeChain(
&.{ Event, UIEvent, MouseEvent, @TypeOf(child) },
).allocate(allocator);
).allocate(arena);
// Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0);
event_ptr.* = try eventInit(typ, chain.get(1), self._page);
event_ptr.* = try self.eventInit(arena, typ, chain.get(1));
chain.setMiddle(1, UIEvent.Type);
// Set MouseEvent with all its fields
@@ -239,6 +221,20 @@ pub fn mouseEvent(self: *Factory, typ: []const u8, mouse: MouseEvent, child: any
return chain.get(3);
}
fn eventInit(self: *const Factory, arena: Allocator, typ: String, value: anytype) !Event {
// Round to 2ms for privacy (browsers do this)
const raw_timestamp = @import("../datetime.zig").milliTimestamp(.monotonic);
const time_stamp = (raw_timestamp / 2) * 2;
return .{
._arena = arena,
._page = self._page,
._type = unionInit(Event.Type, value),
._type_string = typ,
._time_stamp = time_stamp,
};
}
pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator();

View File

@@ -653,7 +653,8 @@ pub fn documentIsLoaded(self: *Page) void {
}
pub fn _documentIsLoaded(self: *Page) !void {
const event = try Event.initTrusted("DOMContentLoaded", .{ .bubbles = true }, self);
const event = try Event.initTrusted(.wrap("DOMContentLoaded"), .{ .bubbles = true }, self);
defer if (!event._v8_handoff) event.deinit(false);
try self._event_manager.dispatch(
self.document.asEventTarget(),
event,
@@ -704,7 +705,9 @@ fn _documentIsComplete(self: *Page) !void {
// Dispatch `_to_load` events before window.load.
for (self._to_load.items) |element| {
const event = try Event.initTrusted("load", .{}, self);
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self);
defer if (!event._v8_handoff) event.deinit(false);
// Dispatch inline event.
blk: {
const html_element = element.is(HtmlElement) orelse break :blk;
@@ -723,7 +726,8 @@ fn _documentIsComplete(self: *Page) !void {
self._to_load.clearAndFree(self.arena);
// Dispatch window.load event.
const event = try Event.initTrusted("load", .{}, self);
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self);
defer if (!event._v8_handoff) event.deinit(false);
// This event is weird, it's dispatched directly on the window, but
// with the document as the target.
event._target = self.document.asEventTarget();
@@ -734,10 +738,11 @@ fn _documentIsComplete(self: *Page) !void {
.{ .inject_target = false, .context = "page load" },
);
const pageshow_event = try PageTransitionEvent.initTrusted("pageshow", .{}, self);
const pageshow_event = (try PageTransitionEvent.initTrusted(comptime .wrap("pageshow"), .{}, self)).asEvent();
defer if (!pageshow_event._v8_handoff) pageshow_event.deinit(false);
try self._event_manager.dispatchWithFunction(
self.window.asEventTarget(),
pageshow_event.asEvent(),
pageshow_event,
ls.toLocal(self.window._on_pageshow),
.{ .context = "page show" },
);
@@ -1443,10 +1448,12 @@ pub fn deliverSlotchangeEvents(self: *Page) void {
self._slots_pending_slotchange.clearRetainingCapacity();
for (slots) |slot| {
const event = Event.initTrusted("slotchange", .{ .bubbles = true }, self) catch |err| {
const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, self) catch |err| {
log.err(.page, "deliverSlotchange.init", .{ .err = err });
continue;
};
defer if (!event._v8_handoff) event.deinit(false);
const target = slot.asNode().asEventTarget();
_ = target.dispatchEvent(event, self) catch |err| {
log.err(.page, "deliverSlotchange.dispatch", .{ .err = err });
@@ -3032,14 +3039,16 @@ pub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void {
.y = y,
});
}
const event = try @import("webapi/event/MouseEvent.zig").init("click", .{
const event = (try @import("webapi/event/MouseEvent.zig").init("click", .{
.bubbles = true,
.cancelable = true,
.composed = true,
.clientX = x,
.clientY = y,
}, self);
try self._event_manager.dispatch(target.asEventTarget(), event.asEvent());
}, self)).asEvent();
defer if (!event._v8_handoff) event.deinit(false);
try self._event_manager.dispatch(target.asEventTarget(), event);
}
// callback when the "click" event reaches the pages.
@@ -3091,6 +3100,9 @@ pub fn handleClick(self: *Page, target: *Node) !void {
}
pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
const event = keyboard_event.asEvent();
defer if (!event._v8_handoff) event.deinit(false);
const element = self.window._document._active_element orelse return;
if (comptime IS_DEBUG) {
log.debug(.page, "page keydown", .{
@@ -3099,7 +3111,7 @@ pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
.key = keyboard_event._key,
});
}
try self._event_manager.dispatch(element.asEventTarget(), keyboard_event.asEvent());
try self._event_manager.dispatch(element.asEventTarget(), event);
}
pub fn handleKeydown(self: *Page, target: *Node, event: *Event) !void {
@@ -3161,7 +3173,9 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
const form_element = form.asElement();
if (submit_opts.fire_event) {
const submit_event = try Event.initTrusted("submit", .{ .bubbles = true, .cancelable = true }, self);
const submit_event = try Event.initTrusted(comptime .wrap("submit"), .{ .bubbles = true, .cancelable = true }, self);
defer if (!submit_event._v8_handoff) submit_event.deinit(false);
const onsubmit_handler = try form.asHtmlElement().getOnSubmit(self);
var ls: JS.Local.Scope = undefined;

View File

@@ -869,7 +869,7 @@ pub const Script = struct {
const cb = cb_ orelse return;
const Event = @import("webapi/Event.zig");
const event = Event.initTrusted(typ, .{}, page) catch |err| {
const event = Event.initTrusted(comptime .wrap(typ), .{}, page) catch |err| {
log.warn(.js, "script internal callback", .{
.url = self.url,
.type = typ,
@@ -877,6 +877,7 @@ pub const Script = struct {
});
return;
};
defer if (!event._v8_handoff) event.deinit(false);
var caught: js.TryCatch.Caught = undefined;
cb.tryCall(void, .{event}, &caught) catch {

View File

@@ -84,7 +84,8 @@ identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Any type that is stored in the identity_map which has a finalizer declared
// will have its finalizer stored here. This is only used when shutting down
// if v8 hasn't called the finalizer directly itself.
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, FinalizerCallback) = .empty,
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
finalizer_callback_pool: std.heap.MemoryPool(FinalizerCallback),
// Some web APIs have to manage opaque values. Ideally, they use an
// js.Object, but the js.Object has no lifetime guarantee beyond the
@@ -196,8 +197,9 @@ pub fn deinit(self: *Context) void {
{
var it = self.finalizer_callbacks.valueIterator();
while (it.next()) |finalizer| {
finalizer.deinit();
finalizer.*.deinit();
}
self.finalizer_callback_pool.deinit();
}
for (self.global_values.items) |*global| {
@@ -246,37 +248,37 @@ pub fn deinit(self: *Context) void {
}
pub fn weakRef(self: *Context, obj: anytype) void {
const global = self.identity_map.getPtr(@intFromPtr(obj)) orelse {
const fc = self.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
v8.v8__Global__SetWeakFinalizer(global, obj, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter);
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter);
}
pub fn safeWeakRef(self: *Context, obj: anytype) void {
const global = self.identity_map.getPtr(@intFromPtr(obj)) orelse {
const fc = self.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
v8.v8__Global__ClearWeak(global);
v8.v8__Global__SetWeakFinalizer(global, obj, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter);
v8.v8__Global__ClearWeak(&fc.global);
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter);
}
pub fn strongRef(self: *Context, obj: anytype) void {
const global = self.identity_map.getPtr(@intFromPtr(obj)) orelse {
const fc = self.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
v8.v8__Global__ClearWeak(global);
v8.v8__Global__ClearWeak(&fc.global);
}
pub fn release(self: *Context, item: anytype) void {
@@ -294,12 +296,14 @@ pub fn release(self: *Context, item: anytype) void {
// The item has been fianalized, remove it for the finalizer callback so that
// we don't try to call it again on shutdown.
_ = self.finalizer_callbacks.fetchRemove(@intFromPtr(item)) orelse {
const fc = self.finalizer_callbacks.fetchRemove(@intFromPtr(item)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
self.finalizer_callback_pool.destroy(fc.value);
return;
}
@@ -1004,29 +1008,31 @@ pub fn queueMicrotaskFunc(self: *Context, cb: js.Function) void {
self.isolate.enqueueMicrotaskFunc(cb);
}
pub fn createFinalizerCallback(self: *Context, global: v8.Global, ptr: *anyopaque, finalizerFn: *const fn (ptr: *anyopaque) void) !*FinalizerCallback {
const fc = try self.finalizer_callback_pool.create();
fc.* = .{
.ctx = self,
.ptr = ptr,
.global = global,
.finalizerFn = finalizerFn,
};
return fc;
}
// == Misc ==
// A type that has a finalizer can have its finalizer called one of two ways.
// The first is from V8 via the WeakCallback we give to weakRef. But that isn't
// guaranteed to fire, so we track this in ctx._finalizers and call them on
// context shutdown.
const FinalizerCallback = struct {
pub const FinalizerCallback = struct {
ctx: *Context,
ptr: *anyopaque,
global: v8.Global,
finalizerFn: *const fn (ptr: *anyopaque) void,
pub fn init(ptr: anytype) FinalizerCallback {
const T = bridge.Struct(@TypeOf(ptr));
return .{
.ptr = ptr,
.finalizerFn = struct {
pub fn wrap(self: *anyopaque) void {
T.JsApi.Meta.finalizer.from_zig(self);
}
}.wrap,
};
}
pub fn deinit(self: FinalizerCallback) void {
pub fn deinit(self: *FinalizerCallback) void {
self.finalizerFn(self.ptr);
self.ctx.finalizer_callback_pool.destroy(self);
}
};

View File

@@ -243,6 +243,7 @@ pub fn createContext(self: *Env, page: *Page, enter: bool) !*Context {
.call_arena = page.call_arena,
.script_manager = &page._script_manager,
.scheduler = .init(context_arena),
.finalizer_callback_pool = std.heap.MemoryPool(Context.FinalizerCallback).init(self.app.allocator),
};
try context.identity_map.putNoClobber(context_arena, @intFromPtr(page.window), global_global);

View File

@@ -198,21 +198,28 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
// context.global_objects, we want to track it in context.identity_map.
v8.v8__Global__New(isolate.handle, js_obj.handle, gop.value_ptr);
if (@hasDecl(JsApi.Meta, "finalizer")) {
if (comptime IS_DEBUG) {
// You can normally return a "*Node" and we'll correctly
// handle it as what it really is, e.g. an HTMLScriptElement.
// But for finalizers, we can't do that. I think this
// limitation will be OK - this auto-resolution is largely
// limited to Node -> HtmlElement, none of which has finalizers
std.debug.assert(resolved.class_id == JsApi.Meta.class_id);
// It would be great if resolved knew the resolved type, but I
// can't figure out how to make that work, since it depends on
// the [runtime] `value`.
// We need the resolved finalizer, which we have in resolved.
// The above if statement would be more clear as:
// if (resolved.finalizer_from_v8) |finalizer| {
// But that's a runtime check.
// Instead, we check if the base has finalizer. The assumption
// here is that if a resolve type has a finalizer, than the base
// should have a finalizer too.
const fc = try ctx.createFinalizerCallback(gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?);
{
errdefer fc.deinit();
try ctx.finalizer_callbacks.put(ctx.arena, @intFromPtr(resolved.ptr), fc);
}
try ctx.finalizer_callbacks.put(ctx.arena, @intFromPtr(resolved.ptr), .init(value));
conditionallyFlagHandoff(value);
if (@hasDecl(JsApi.Meta, "weak")) {
if (comptime IS_DEBUG) {
std.debug.assert(JsApi.Meta.weak == true);
}
v8.v8__Global__SetWeakFinalizer(gop.value_ptr, resolved.ptr, JsApi.Meta.finalizer.from_v8, v8.kParameter);
v8.v8__Global__SetWeakFinalizer(gop.value_ptr, fc, resolved.finalizer_from_v8, v8.kParameter);
}
}
return js_obj;
@@ -1026,9 +1033,12 @@ fn jsUnsignedIntToZig(comptime T: type, max: comptime_int, maybe: u32) !T {
// This function recursively walks the _type union field (if there is one) to
// get the most specific class_id possible.
const Resolved = struct {
weak: bool,
ptr: *anyopaque,
class_id: u16,
prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry,
finalizer_from_v8: ?*const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void = null,
finalizer_from_zig: ?*const fn (ptr: *anyopaque) void = null,
};
pub fn resolveValue(value: anytype) Resolved {
const T = bridge.Struct(@TypeOf(value));
@@ -1056,13 +1066,28 @@ pub fn resolveValue(value: anytype) Resolved {
}
fn resolveT(comptime T: type, value: *anyopaque) Resolved {
const Meta = T.JsApi.Meta;
return .{
.ptr = value,
.class_id = T.JsApi.Meta.class_id,
.prototype_chain = &T.JsApi.Meta.prototype_chain,
.class_id = Meta.class_id,
.prototype_chain = &Meta.prototype_chain,
.weak = if (@hasDecl(Meta, "weak")) Meta.weak else false,
.finalizer_from_v8 = if (@hasDecl(Meta, "finalizer")) Meta.finalizer.from_v8 else null,
.finalizer_from_zig = if (@hasDecl(Meta, "finalizer")) Meta.finalizer.from_zig else null,
};
}
fn conditionallyFlagHandoff(value: anytype) void {
const T = bridge.Struct(@TypeOf(value));
if (@hasField(T, "_v8_handoff")) {
value._v8_handoff = true;
return;
}
if (@hasField(T, "_proto")) {
conditionallyFlagHandoff(value._proto);
}
}
pub fn stackTrace(self: *const Local) !?[]const u8 {
const isolate = self.isolate;
const separator = log.separator();

View File

@@ -115,20 +115,18 @@ pub fn Builder(comptime T: type) type {
.from_v8 = struct {
fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
const self: *T = @ptrCast(@alignCast(ptr));
// This is simply a requirement of any type that Finalizes:
// It must have a _page: *Page field. We need it because
// we need to check the item has already been cleared
// (There are all types of weird timing issues that seem
// to be possible between finalization and context shutdown,
// we need to be defensive).
// There _ARE_ alternatives to this. But this is simple.
const ctx = self._page.js;
if (!ctx.identity_map.contains(@intFromPtr(ptr))) {
return;
const fc: *Context.FinalizerCallback = @ptrCast(@alignCast(ptr));
const ctx = fc.ctx;
const value_ptr = fc.ptr;
if (ctx.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
func(@ptrCast(@alignCast(value_ptr)), false);
ctx.release(value_ptr);
} else {
// A bit weird, but v8 _requires_ that we release it
// If we don't. We'll 100% crash.
v8.v8__Global__Reset(&fc.global);
}
func(self, false);
ctx.release(ptr);
}
}.wrap,
};

View File

@@ -76,7 +76,9 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, local: *const js.Local, page:
}
// Dispatch abort event
const event = try Event.initTrusted("abort", .{}, page);
const event = try Event.initTrusted(comptime .wrap("abort"), .{}, page);
defer if (!event._v8_handoff) event.deinit(false);
try page._event_manager.dispatchWithFunction(
self.asEventTarget(),
event,

View File

@@ -767,7 +767,8 @@ pub fn focus(self: *Element, page: *Page) !void {
return;
}
const blur_event = try Event.initTrusted("blur", null, page);
const blur_event = try Event.initTrusted(comptime .wrap("blur"), null, page);
defer if (!blur_event._v8_handoff) blur_event.deinit(false);
try page._event_manager.dispatch(old.asEventTarget(), blur_event);
}
@@ -775,7 +776,8 @@ pub fn focus(self: *Element, page: *Page) !void {
page.document._active_element = self;
}
const focus_event = try Event.initTrusted("focus", null, page);
const focus_event = try Event.initTrusted(comptime .wrap("focus"), null, page);
defer if (!focus_event._v8_handoff) focus_event.deinit(false);
try page._event_manager.dispatch(self.asEventTarget(), focus_event);
}
@@ -785,7 +787,8 @@ pub fn blur(self: *Element, page: *Page) !void {
page.document._active_element = null;
const Event = @import("Event.zig");
const blur_event = try Event.initTrusted("blur", null, page);
const blur_event = try Event.initTrusted(comptime .wrap("blur"), null, page);
defer if (!blur_event._v8_handoff) blur_event.deinit(false);
try page._event_manager.dispatch(self.asEventTarget(), blur_event);
}

View File

@@ -24,11 +24,14 @@ const EventTarget = @import("EventTarget.zig");
const Node = @import("Node.zig");
const String = @import("../../string.zig").String;
const Allocator = std.mem.Allocator;
pub const Event = @This();
pub const _prototype_root = true;
_type: Type,
_page: *Page,
_arena: Allocator,
_bubbles: bool = false,
_cancelable: bool = false,
_composed: bool = false,
@@ -44,6 +47,12 @@ _time_stamp: u64,
_needs_retargeting: bool = false,
_isTrusted: bool = false,
// There's a period of time between creating an event and handing it off to v8
// where things can fail. If it does fail, we need to deinit the event. This flag
// when true, tells us the event is registered in the js.Contxt and thus, at
// the very least, will be finalized on context shutdown.
_v8_handoff: bool = false,
pub const EventPhase = enum(u8) {
none = 0,
capturing_phase = 1,
@@ -70,31 +79,38 @@ pub const Options = struct {
composed: bool = false,
};
pub fn initTrusted(typ: []const u8, opts_: ?Options, page: *Page) !*Event {
return initWithTrusted(typ, opts_, true, page);
}
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event {
return initWithTrusted(typ, opts_, false, page);
const arena = try page.getArena(.{ .debug = "Event" });
errdefer page.releaseArena(arena);
const str = try String.init(arena, typ, .{});
return initWithTrusted(arena, str, opts_, false, page);
}
fn initWithTrusted(typ: []const u8, opts_: ?Options, trusted: bool, page: *Page) !*Event {
pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*Event {
const arena = try page.getArena(.{ .debug = "Event.trusted" });
errdefer page.releaseArena(arena);
return initWithTrusted(arena, typ, opts_, true, page);
}
fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, page: *Page) !*Event {
const opts = opts_ orelse Options{};
// Round to 2ms for privacy (browsers do this)
const raw_timestamp = @import("../../datetime.zig").milliTimestamp(.monotonic);
const time_stamp = (raw_timestamp / 2) * 2;
const event = try page._factory.create(Event{
const event = try arena.create(Event);
event.* = .{
._page = page,
._arena = arena,
._type = .generic,
._bubbles = opts.bubbles,
._time_stamp = time_stamp,
._cancelable = opts.cancelable,
._composed = opts.composed,
._type_string = try String.init(page.arena, typ, .{}),
});
event._isTrusted = trusted;
._type_string = typ,
._isTrusted = trusted,
};
return event;
}
@@ -103,18 +119,22 @@ pub fn initEvent(
event_string: []const u8,
bubbles: ?bool,
cancelable: ?bool,
page: *Page,
) !void {
if (self._event_phase != .none) {
return;
}
self._type_string = try String.init(page.arena, event_string, .{});
self._type_string = try String.init(self._arena, event_string, .{});
self._bubbles = bubbles orelse false;
self._cancelable = cancelable orelse false;
self._stop_propagation = false;
}
pub fn deinit(self: *Event, shutdown: bool) void {
_ = shutdown;
self._page.releaseArena(self._arena);
}
pub fn as(self: *Event, comptime T: type) *T {
return self.is(T).?;
}
@@ -385,6 +405,8 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Event.deinit);
};
pub const constructor = bridge.constructor(Event.init, .{});

View File

@@ -79,11 +79,12 @@ fn goInner(delta: i32, page: *Page) !void {
if (entry._url) |url| {
if (try page.isSameOrigin(url)) {
const event = try PopStateEvent.initTrusted("popstate", .{ .state = entry._state.value }, page);
const event = (try PopStateEvent.initTrusted(comptime .wrap("popstate"), .{ .state = entry._state.value }, page)).asEvent();
defer if (!event._v8_handoff) event.deinit(false);
try page._event_manager.dispatchWithFunction(
page.window.asEventTarget(),
event.asEvent(),
event,
page.js.toLocal(page.window._on_popstate),
.{ .context = "Pop State" },
);

View File

@@ -122,14 +122,15 @@ const PostMessageCallback = struct {
return null;
}
const event = MessageEvent.initTrusted("message", .{
const event = (MessageEvent.initTrusted(comptime .wrap("message"), .{
.data = self.message,
.origin = "",
.source = null,
}, page) catch |err| {
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
return null;
};
}).asEvent();
defer if (!event._v8_handoff) event.deinit(false);
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
@@ -137,7 +138,7 @@ const PostMessageCallback = struct {
page._event_manager.dispatchWithFunction(
self.port.asEventTarget(),
event.asEvent(),
event,
ls.toLocal(self.port._on_message),
.{ .context = "MessagePort message" },
) catch |err| {

View File

@@ -277,7 +277,7 @@ pub fn cancelIdleCallback(self: *Window, id: u32) void {
}
pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
const error_event = try ErrorEvent.initTrusted("error", .{
const error_event = try ErrorEvent.initTrusted(comptime .wrap("error"), .{
.@"error" = try err.persist(),
.message = err.toStringSlice() catch "Unknown error",
.bubbles = false,
@@ -285,6 +285,7 @@ pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
}, page);
const event = error_event.asEvent();
defer if (!event._v8_handoff) event.deinit(false);
// Invoke window.onerror callback if set (per WHATWG spec, this is called
// with 5 arguments: message, source, lineno, colno, error)
@@ -443,7 +444,8 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
return null;
}
const event = try Event.initTrusted("scroll", .{ .bubbles = true }, p);
const event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, p);
defer if (!event._v8_handoff) event.deinit(false);
try p._event_manager.dispatch(p.document.asEventTarget(), event);
pos.state = .end;
@@ -470,7 +472,8 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
.end => {},
.done => return null,
}
const event = try Event.initTrusted("scrollend", .{ .bubbles = true }, p);
const event = try Event.initTrusted(comptime .wrap("scrollend"), .{ .bubbles = true }, p);
defer if (!event._v8_handoff) event.deinit(false);
try p._event_manager.dispatch(p.document.asEventTarget(), event);
pos.state = .done;
@@ -640,15 +643,14 @@ const PostMessageCallback = struct {
const page = self.page;
const window = page.window;
const message_event = try MessageEvent.initTrusted("message", .{
const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{
.data = self.message,
.origin = self.origin,
.source = window,
.bubbles = false,
.cancelable = false,
}, page);
const event = message_event.asEvent();
}, page)).asEvent();
defer if (!event._v8_handoff) event.deinit(false);
try page._event_manager.dispatch(window.asEventTarget(), event);
return null;

View File

@@ -15,11 +15,13 @@
//
// 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 String = @import("../../..//string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator;
const CompositionEvent = @This();
@@ -33,17 +35,28 @@ const CompositionEventOptions = struct {
const Options = Event.inheritOptions(CompositionEvent, CompositionEventOptions);
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CompositionEvent {
const opts = opts_ orelse Options{};
const arena = try page.getArena(.{ .debug = "CompositionEvent" });
errdefer page.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
const event = try page._factory.event(typ, CompositionEvent{
._proto = undefined,
._data = if (opts.data) |str| try page.dupeString(str) else "",
});
const opts = opts_ orelse Options{};
const event = try page._factory.event(
arena,
type_string,
CompositionEvent{
._proto = undefined,
._data = if (opts.data) |str| try arena.dupe(u8, str) else "",
},
);
Event.populatePrototypes(event, opts, false);
return event;
}
pub fn deinit(self: *CompositionEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *CompositionEvent) *Event {
return self._proto;
}
@@ -59,6 +72,8 @@ pub const JsApi = struct {
pub const name = "CompositionEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(CompositionEvent.deinit);
};
pub const constructor = bridge.constructor(CompositionEvent.init, .{});

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -17,9 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../../js/js.zig");
const String = @import("../../..//string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator;
@@ -37,11 +37,14 @@ const CustomEventOptions = struct {
const Options = Event.inheritOptions(CustomEvent, CustomEventOptions);
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CustomEvent {
const arena = page.arena;
const opts = opts_ orelse Options{};
const arena = try page.getArena(.{ .debug = "CustomEvent" });
errdefer page.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
const opts = opts_ orelse Options{};
const event = try page._factory.event(
typ,
arena,
type_string,
CustomEvent{
._arena = arena,
._proto = undefined,
@@ -59,17 +62,20 @@ pub fn initCustomEvent(
bubbles: ?bool,
cancelable: ?bool,
detail_: ?js.Value.Global,
page: *Page,
) !void {
// This function can only be called after the constructor has called.
// So we assume proto is initialized already by constructor.
self._proto._type_string = try String.init(page.arena, event_string, .{});
self._proto._type_string = try String.init(self._proto._arena, event_string, .{});
self._proto._bubbles = bubbles orelse false;
self._proto._cancelable = cancelable orelse false;
// Detail is stored separately.
self._detail = detail_;
}
pub fn deinit(self: *CustomEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *CustomEvent) *Event {
return self._proto;
}
@@ -85,6 +91,8 @@ pub const JsApi = struct {
pub const name = "CustomEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(CustomEvent.deinit);
};
pub const constructor = bridge.constructor(CustomEvent.init, .{});

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -17,9 +17,11 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../../js/js.zig");
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator;
@@ -44,18 +46,23 @@ pub const ErrorEventOptions = struct {
const Options = Event.inheritOptions(ErrorEvent, ErrorEventOptions);
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*ErrorEvent {
return initWithTrusted(typ, opts_, false, page);
const arena = try page.getArena(.{ .debug = "ErrorEvent" });
errdefer page.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
return initWithTrusted(arena, type_string, opts_, false, page);
}
pub fn initTrusted(typ: []const u8, opts_: ?Options, page: *Page) !*ErrorEvent {
return initWithTrusted(typ, opts_, true, page);
pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*ErrorEvent {
const arena = try page.getArena(.{ .debug = "ErrorEvent.trusted" });
errdefer page.releaseArena(arena);
return initWithTrusted(arena, typ, opts_, true, page);
}
fn initWithTrusted(typ: []const u8, opts_: ?Options, trusted: bool, page: *Page) !*ErrorEvent {
const arena = page.arena;
fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, page: *Page) !*ErrorEvent {
const opts = opts_ orelse Options{};
const event = try page._factory.event(
arena,
typ,
ErrorEvent{
._arena = arena,
@@ -72,6 +79,10 @@ fn initWithTrusted(typ: []const u8, opts_: ?Options, trusted: bool, page: *Page)
return event;
}
pub fn deinit(self: *ErrorEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *ErrorEvent) *Event {
return self._proto;
}
@@ -103,6 +114,8 @@ pub const JsApi = struct {
pub const name = "ErrorEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(ErrorEvent.deinit);
};
// Start API

View File

@@ -17,10 +17,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Event = @import("../Event.zig");
const UIEvent = @import("UIEvent.zig");
const Page = @import("../../Page.zig");
const js = @import("../../js/js.zig");
const Allocator = std.mem.Allocator;
const KeyboardEvent = @This();
@@ -180,24 +184,30 @@ const Options = Event.inheritOptions(
KeyboardEventOptions,
);
pub fn initTrusted(typ: []const u8, _opts: ?Options, page: *Page) !*KeyboardEvent {
return initWithTrusted(typ, _opts, true, page);
pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*KeyboardEvent {
const arena = try page.getArena(.{ .debug = "KeyboardEvent.trusted" });
errdefer page.releaseArena(arena);
return initWithTrusted(arena, typ, _opts, true, page);
}
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*KeyboardEvent {
return initWithTrusted(typ, _opts, false, page);
const arena = try page.getArena(.{ .debug = "KeyboardEvent" });
errdefer page.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
return initWithTrusted(arena, type_string, _opts, false, page);
}
fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, page: *Page) !*KeyboardEvent {
fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*KeyboardEvent {
const opts = _opts orelse Options{};
const event = try page._factory.uiEvent(
arena,
typ,
KeyboardEvent{
._proto = undefined,
._key = try Key.fromString(page.arena, opts.key),
._key = try Key.fromString(arena, opts.key),
._location = std.meta.intToEnum(Location, opts.location) catch return error.TypeError,
._code = if (opts.code) |c| try page.dupeString(c) else "",
._code = if (opts.code) |c| try arena.dupe(u8, c) else "",
._repeat = opts.repeat,
._is_composing = opts.isComposing,
._ctrl_key = opts.ctrlKey,
@@ -211,6 +221,10 @@ fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, page: *Page)
return event;
}
pub fn deinit(self: *KeyboardEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *KeyboardEvent) *Event {
return self._proto.asEvent();
}
@@ -251,8 +265,8 @@ pub fn getShiftKey(self: *const KeyboardEvent) bool {
return self._shift_key;
}
pub fn getModifierState(self: *const KeyboardEvent, str: []const u8, page: *Page) !bool {
const key = try Key.fromString(page.arena, str);
pub fn getModifierState(self: *const KeyboardEvent, str: []const u8) !bool {
const key = try Key.fromString(self._proto._proto._arena, str);
switch (key) {
.Alt, .AltGraph => return self._alt_key,
@@ -274,6 +288,8 @@ pub const JsApi = struct {
pub const name = "KeyboardEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(KeyboardEvent.deinit);
};
pub const constructor = bridge.constructor(KeyboardEvent.init, .{});

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -16,11 +16,15 @@
// 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 String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Event = @import("../Event.zig");
const Window = @import("../Window.zig");
const Allocator = std.mem.Allocator;
const MessageEvent = @This();
@@ -38,22 +42,28 @@ const MessageEventOptions = struct {
const Options = Event.inheritOptions(MessageEvent, MessageEventOptions);
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*MessageEvent {
return initWithTrusted(typ, opts_, false, page);
const arena = try page.getArena(.{ .debug = "MessageEvent" });
errdefer page.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
return initWithTrusted(arena, type_string, opts_, false, page);
}
pub fn initTrusted(typ: []const u8, opts_: ?Options, page: *Page) !*MessageEvent {
return initWithTrusted(typ, opts_, true, page);
pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*MessageEvent {
const arena = try page.getArena(.{ .debug = "MessageEvent.trusted" });
errdefer page.releaseArena(arena);
return initWithTrusted(arena, typ, opts_, true, page);
}
fn initWithTrusted(typ: []const u8, opts_: ?Options, trusted: bool, page: *Page) !*MessageEvent {
fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, page: *Page) !*MessageEvent {
const opts = opts_ orelse Options{};
const event = try page._factory.event(
arena,
typ,
MessageEvent{
._proto = undefined,
._data = opts.data,
._origin = if (opts.origin) |str| try page.arena.dupe(u8, str) else "",
._origin = if (opts.origin) |str| try arena.dupe(u8, str) else "",
._source = opts.source,
},
);
@@ -62,6 +72,10 @@ fn initWithTrusted(typ: []const u8, opts_: ?Options, trusted: bool, page: *Page)
return event;
}
pub fn deinit(self: *MessageEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *MessageEvent) *Event {
return self._proto;
}
@@ -85,6 +99,8 @@ pub const JsApi = struct {
pub const name = "MessageEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MessageEvent.deinit);
};
pub const constructor = bridge.constructor(MessageEvent.init, .{});

View File

@@ -17,11 +17,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Event = @import("../Event.zig");
const UIEvent = @import("UIEvent.zig");
const EventTarget = @import("../EventTarget.zig");
const String = @import("../../../string.zig").String;
const Page = @import("../../Page.zig");
const js = @import("../../js/js.zig");
const Event = @import("../Event.zig");
const EventTarget = @import("../EventTarget.zig");
const UIEvent = @import("UIEvent.zig");
const PointerEvent = @import("PointerEvent.zig");
const MouseEvent = @This();
@@ -75,10 +78,15 @@ pub const Options = Event.inheritOptions(
);
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*MouseEvent {
const arena = try page.getArena(.{ .debug = "MouseEvent" });
errdefer page.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
const opts = _opts orelse Options{};
const event = try page._factory.uiEvent(
typ,
arena,
type_string,
MouseEvent{
._type = .generic,
._proto = undefined,
@@ -99,6 +107,10 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*MouseEvent {
return event;
}
pub fn deinit(self: *MouseEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *MouseEvent) *Event {
return self._proto.asEvent();
}
@@ -172,6 +184,8 @@ pub const JsApi = struct {
pub const name = "MouseEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MouseEvent.deinit);
};
pub const constructor = bridge.constructor(MouseEvent.init, .{});

View File

@@ -17,11 +17,15 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Event = @import("../Event.zig");
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Event = @import("../Event.zig");
const NavigationHistoryEntry = @import("../navigation/NavigationHistoryEntry.zig");
const NavigationType = @import("../navigation/root.zig").NavigationType;
const js = @import("../../js/js.zig");
const Allocator = std.mem.Allocator;
const NavigationCurrentEntryChangeEvent = @This();
@@ -40,15 +44,21 @@ const Options = Event.inheritOptions(
);
pub fn init(typ: []const u8, opts: Options, page: *Page) !*NavigationCurrentEntryChangeEvent {
return initWithTrusted(typ, opts, false, page);
const arena = try page.getArena(.{ .debug = "NavigationCurrentEntryChangeEvent" });
errdefer page.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
return initWithTrusted(arena, type_string, opts, false, page);
}
pub fn initTrusted(typ: []const u8, opts: Options, page: *Page) !*NavigationCurrentEntryChangeEvent {
return initWithTrusted(typ, opts, true, page);
pub fn initTrusted(typ: String, opts: Options, page: *Page) !*NavigationCurrentEntryChangeEvent {
const arena = try page.getArena(.{ .debug = "NavigationCurrentEntryChangeEvent.trusted" });
errdefer page.releaseArena(arena);
return initWithTrusted(arena, typ, opts, true, page);
}
fn initWithTrusted(
typ: []const u8,
arena: Allocator,
typ: String,
opts: Options,
trusted: bool,
page: *Page,
@@ -59,6 +69,7 @@ fn initWithTrusted(
null;
const event = try page._factory.event(
arena,
typ,
NavigationCurrentEntryChangeEvent{
._proto = undefined,
@@ -71,6 +82,10 @@ fn initWithTrusted(
return event;
}
pub fn deinit(self: *NavigationCurrentEntryChangeEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *NavigationCurrentEntryChangeEvent) *Event {
return self._proto;
}
@@ -90,6 +105,8 @@ pub const JsApi = struct {
pub const name = "NavigationCurrentEntryChangeEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(NavigationCurrentEntryChangeEvent.deinit);
};
pub const constructor = bridge.constructor(NavigationCurrentEntryChangeEvent.init, .{});

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -16,9 +16,13 @@
// 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 Event = @import("../Event.zig");
const std = @import("std");
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator;
// https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent
const PageTransitionEvent = @This();
@@ -33,17 +37,23 @@ const PageTransitionEventOptions = struct {
const Options = Event.inheritOptions(PageTransitionEvent, PageTransitionEventOptions);
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PageTransitionEvent {
return initWithTrusted(typ, _opts, false, page);
const arena = try page.getArena(.{ .debug = "PageTransitionEvent" });
errdefer page.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
return initWithTrusted(arena, type_string, _opts, false, page);
}
pub fn initTrusted(typ: []const u8, _opts: ?Options, page: *Page) !*PageTransitionEvent {
return initWithTrusted(typ, _opts, true, page);
pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*PageTransitionEvent {
const arena = try page.getArena(.{ .debug = "PageTransitionEvent.trusted" });
errdefer page.releaseArena(arena);
return initWithTrusted(arena, typ, _opts, true, page);
}
fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, page: *Page) !*PageTransitionEvent {
fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*PageTransitionEvent {
const opts = _opts orelse Options{};
const event = try page._factory.event(
arena,
typ,
PageTransitionEvent{
._proto = undefined,
@@ -55,6 +65,10 @@ fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, page: *Page)
return event;
}
pub fn deinit(self: *PageTransitionEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *PageTransitionEvent) *Event {
return self._proto;
}
@@ -70,6 +84,8 @@ pub const JsApi = struct {
pub const name = "PageTransitionEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PageTransitionEvent.deinit);
};
pub const constructor = bridge.constructor(PageTransitionEvent.init, .{});

View File

@@ -17,6 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Event = @import("../Event.zig");
@@ -81,10 +83,14 @@ const Options = Event.inheritOptions(
);
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PointerEvent {
const opts = _opts orelse Options{};
const arena = try page.getArena(.{ .debug = "UIEvent" });
errdefer page.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
const opts = _opts orelse Options{};
const event = try page._factory.mouseEvent(
typ,
arena,
type_string,
MouseEvent{
._type = .{ .pointer_event = undefined },
._proto = undefined,
@@ -120,6 +126,10 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PointerEvent {
return event;
}
pub fn deinit(self: *PointerEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *PointerEvent) *Event {
return self._proto.asEvent();
}
@@ -179,6 +189,8 @@ pub const JsApi = struct {
pub const name = "PointerEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PointerEvent.deinit);
};
pub const constructor = bridge.constructor(PointerEvent.init, .{});

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -16,10 +16,15 @@
// 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 Event = @import("../Event.zig");
const std = @import("std");
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator;
// https://developer.mozilla.org/en-US/docs/Web/API/PopStateEvent
const PopStateEvent = @This();
@@ -33,17 +38,23 @@ const PopStateEventOptions = struct {
const Options = Event.inheritOptions(PopStateEvent, PopStateEventOptions);
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PopStateEvent {
return initWithTrusted(typ, _opts, false, page);
const arena = try page.getArena(.{ .debug = "PopStateEvent" });
errdefer page.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
return initWithTrusted(arena, type_string, _opts, false, page);
}
pub fn initTrusted(typ: []const u8, _opts: ?Options, page: *Page) !*PopStateEvent {
return initWithTrusted(typ, _opts, true, page);
pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*PopStateEvent {
const arena = try page.getArena(.{ .debug = "PopStateEvent.trusted" });
errdefer page.releaseArena(arena);
return initWithTrusted(arena, typ, _opts, true, page);
}
fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, page: *Page) !*PopStateEvent {
fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*PopStateEvent {
const opts = _opts orelse Options{};
const event = try page._factory.event(
arena,
typ,
PopStateEvent{
._proto = undefined,
@@ -55,6 +66,10 @@ fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, page: *Page)
return event;
}
pub fn deinit(self: *PopStateEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *PopStateEvent) *Event {
return self._proto;
}
@@ -76,6 +91,8 @@ pub const JsApi = struct {
pub const name = "PopStateEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PopStateEvent.deinit);
};
pub const constructor = bridge.constructor(PopStateEvent.init, .{});

View File

@@ -16,8 +16,12 @@
// 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 String = @import("../../../string.zig").String;
const Page = @import("../../Page.zig");
const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator;
const ProgressEvent = @This();
_proto: *Event,
@@ -34,17 +38,23 @@ const ProgressEventOptions = struct {
const Options = Event.inheritOptions(ProgressEvent, ProgressEventOptions);
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*ProgressEvent {
return initWithTrusted(typ, _opts, false, page);
const arena = try page.getArena(.{ .debug = "ProgressEvent" });
errdefer page.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
return initWithTrusted(arena, type_string, _opts, false, page);
}
pub fn initTrusted(typ: []const u8, _opts: ?Options, page: *Page) !*ProgressEvent {
return initWithTrusted(typ, _opts, true, page);
pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*ProgressEvent {
const arena = try page.getArena(.{ .debug = "ProgressEvent.trusted" });
errdefer page.releaseArena(arena);
return initWithTrusted(arena, typ, _opts, true, page);
}
fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, page: *Page) !*ProgressEvent {
fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*ProgressEvent {
const opts = _opts orelse Options{};
const event = try page._factory.event(
arena,
typ,
ProgressEvent{
._proto = undefined,
@@ -57,6 +67,10 @@ fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, page: *Page)
return event;
}
pub fn deinit(self: *ProgressEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *ProgressEvent) *Event {
return self._proto;
}
@@ -81,6 +95,8 @@ pub const JsApi = struct {
pub const name = "ProgressEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(ProgressEvent.deinit);
};
pub const constructor = bridge.constructor(ProgressEvent.init, .{});

View File

@@ -16,11 +16,13 @@
// 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 Event = @import("../Event.zig");
const Window = @import("../Window.zig");
const String = @import("../../../string.zig").String;
const Page = @import("../../Page.zig");
const js = @import("../../js/js.zig");
const Event = @import("../Event.zig");
const Window = @import("../Window.zig");
const UIEvent = @This();
_type: Type,
@@ -45,10 +47,14 @@ pub const Options = Event.inheritOptions(
);
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*UIEvent {
const opts = _opts orelse Options{};
const arena = try page.getArena(.{ .debug = "UIEvent" });
errdefer page.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
const opts = _opts orelse Options{};
const event = try page._factory.event(
typ,
arena,
type_string,
UIEvent{
._type = .generic,
._proto = undefined,
@@ -61,6 +67,10 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*UIEvent {
return event;
}
pub fn deinit(self: *UIEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn as(self: *UIEvent, comptime T: type) *T {
return self.is(T).?;
}
@@ -105,6 +115,8 @@ pub const JsApi = struct {
pub const name = "UIEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(UIEvent.deinit);
};
pub const constructor = bridge.constructor(UIEvent.init, .{});

View File

@@ -199,7 +199,7 @@ pub fn pushEntry(
if (previous) |prev| {
if (dispatch) {
const event = try NavigationCurrentEntryChangeEvent.initTrusted(
"currententrychange",
.wrap("currententrychange"),
.{ .from = prev, .navigationType = @tagName(.push) },
page,
);
@@ -238,7 +238,7 @@ pub fn replaceEntry(
if (dispatch) {
const event = try NavigationCurrentEntryChangeEvent.initTrusted(
"currententrychange",
.wrap("currententrychange"),
.{ .from = previous, .navigationType = @tagName(.replace) },
page,
);
@@ -330,7 +330,7 @@ pub fn navigateInner(
// If we haven't navigated off, let us fire off an a currententrychange.
const event = try NavigationCurrentEntryChangeEvent.initTrusted(
"currententrychange",
.wrap("currententrychange"),
.{ .from = previous, .navigationType = @tagName(kind) },
page,
);
@@ -372,7 +372,7 @@ pub fn reload(self: *Navigation, _opts: ?ReloadOptions, page: *Page) !Navigation
entry._state = .{ .source = .navigation, .value = state.toJson(arena) catch return error.DataClone };
const event = try NavigationCurrentEntryChangeEvent.initTrusted(
"currententrychange",
.wrap("currententrychange"),
.{ .from = previous, .navigationType = @tagName(.reload) },
page,
);
@@ -414,7 +414,7 @@ pub fn updateCurrentEntry(self: *Navigation, options: UpdateCurrentEntryOptions,
};
const event = try NavigationCurrentEntryChangeEvent.initTrusted(
"currententrychange",
.wrap("currententrychange"),
.{ .from = previous, .navigationType = null },
page,
);

View File

@@ -46,6 +46,7 @@ pub fn dispatch(self: *NavigationEventTarget, event_type: DispatchType, page: *P
.currententrychange => |cec| .{ cec.asEvent(), "_on_currententrychange" },
};
};
defer if (!event._v8_handoff) event.deinit(false);
if (comptime IS_DEBUG) {
if (page.js.local == null) {

View File

@@ -515,7 +515,9 @@ fn stateChanged(self: *XMLHttpRequest, state: ReadyState, local: *const js.Local
self._ready_state = state;
const event = try Event.initTrusted("readystatechange", .{}, page);
const event = try Event.initTrusted(.wrap("readystatechange"), .{}, page);
defer if (!event._v8_handoff) event.deinit(false);
try page._event_manager.dispatchWithFunction(
self.asEventTarget(),
event,

View File

@@ -57,15 +57,16 @@ pub fn dispatch(self: *XMLHttpRequestEventTarget, comptime event_type: DispatchT
};
const progress = progress_ orelse Progress{};
const event = try ProgressEvent.initTrusted(
typ,
const event = (try ProgressEvent.initTrusted(
comptime .wrap(typ),
.{ .total = progress.total, .loaded = progress.loaded },
page,
);
)).asEvent();
defer if (!event._v8_handoff) event.deinit(false);
return page._event_manager.dispatchWithFunction(
self.asEventTarget(),
event.asEvent(),
event,
local.toLocal(@field(self, field)),
.{ .context = "XHR " ++ typ },
);

View File

@@ -61,7 +61,7 @@ fn dispatchKeyEvent(cmd: anytype) !void {
const page = bc.session.currentPage() orelse return;
const KeyboardEvent = @import("../../browser/webapi/event/KeyboardEvent.zig");
const keyboard_event = try KeyboardEvent.initTrusted("keydown", .{
const keyboard_event = try KeyboardEvent.initTrusted(comptime .wrap("keydown"), .{
.key = params.key,
.code = params.code,
.altKey = params.modifiers & 1 == 1,

View File

@@ -31,7 +31,7 @@ pub fn main() !void {
// allocator
// - in Debug mode we use the General Purpose Allocator to detect memory leaks
// - in Release mode we use the c allocator
var gpa_instance: std.heap.DebugAllocator(.{}) = .init;
var gpa_instance: std.heap.DebugAllocator(.{ .stack_trace_frames = 10 }) = .init;
const gpa = if (builtin.mode == .Debug) gpa_instance.allocator() else std.heap.c_allocator;
defer if (builtin.mode == .Debug) {