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); 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 // this is a root object
pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) { pub fn event(self: *Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator();
const chain = try PrototypeChain( const chain = try PrototypeChain(
&.{ Event, @TypeOf(child) }, &.{ Event, @TypeOf(child) },
).allocate(allocator); ).allocate(arena);
// Special case: Event has a _type_string field, so we need manual setup // Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0); 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); chain.setLeaf(1, child);
return chain.get(1); return chain.get(1);
} }
pub fn uiEvent(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) { pub fn uiEvent(self: *Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator();
const chain = try PrototypeChain( const chain = try PrototypeChain(
&.{ Event, UIEvent, @TypeOf(child) }, &.{ Event, UIEvent, @TypeOf(child) },
).allocate(allocator); ).allocate(arena);
// Special case: Event has a _type_string field, so we need manual setup // Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0); 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.setMiddle(1, UIEvent.Type);
chain.setLeaf(2, child); chain.setLeaf(2, child);
return chain.get(2); return chain.get(2);
} }
pub fn mouseEvent(self: *Factory, typ: []const u8, mouse: MouseEvent, child: anytype) !*@TypeOf(child) { pub fn mouseEvent(self: *Factory, arena: Allocator, typ: String, mouse: MouseEvent, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator();
const chain = try PrototypeChain( const chain = try PrototypeChain(
&.{ Event, UIEvent, MouseEvent, @TypeOf(child) }, &.{ Event, UIEvent, MouseEvent, @TypeOf(child) },
).allocate(allocator); ).allocate(arena);
// Special case: Event has a _type_string field, so we need manual setup // Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0); 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.setMiddle(1, UIEvent.Type);
// Set MouseEvent with all its fields // 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); 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) { pub fn blob(self: *Factory, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator(); const allocator = self._slab.allocator();

View File

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

View File

@@ -869,7 +869,7 @@ pub const Script = struct {
const cb = cb_ orelse return; const cb = cb_ orelse return;
const Event = @import("webapi/Event.zig"); 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", .{ log.warn(.js, "script internal callback", .{
.url = self.url, .url = self.url,
.type = typ, .type = typ,
@@ -877,6 +877,7 @@ pub const Script = struct {
}); });
return; return;
}; };
defer if (!event._v8_handoff) event.deinit(false);
var caught: js.TryCatch.Caught = undefined; var caught: js.TryCatch.Caught = undefined;
cb.tryCall(void, .{event}, &caught) catch { 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 // 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 // will have its finalizer stored here. This is only used when shutting down
// if v8 hasn't called the finalizer directly itself. // 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 // Some web APIs have to manage opaque values. Ideally, they use an
// js.Object, but the js.Object has no lifetime guarantee beyond the // 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(); var it = self.finalizer_callbacks.valueIterator();
while (it.next()) |finalizer| { while (it.next()) |finalizer| {
finalizer.deinit(); finalizer.*.deinit();
} }
self.finalizer_callback_pool.deinit();
} }
for (self.global_values.items) |*global| { for (self.global_values.items) |*global| {
@@ -246,37 +248,37 @@ pub fn deinit(self: *Context) void {
} }
pub fn weakRef(self: *Context, obj: anytype) 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) { if (comptime IS_DEBUG) {
// should not be possible // should not be possible
std.debug.assert(false); std.debug.assert(false);
} }
return; 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 { 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) { if (comptime IS_DEBUG) {
// should not be possible // should not be possible
std.debug.assert(false); std.debug.assert(false);
} }
return; return;
}; };
v8.v8__Global__ClearWeak(global); v8.v8__Global__ClearWeak(&fc.global);
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 strongRef(self: *Context, obj: anytype) void { 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) { if (comptime IS_DEBUG) {
// should not be possible // should not be possible
std.debug.assert(false); std.debug.assert(false);
} }
return; return;
}; };
v8.v8__Global__ClearWeak(global); v8.v8__Global__ClearWeak(&fc.global);
} }
pub fn release(self: *Context, item: anytype) void { 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 // The item has been fianalized, remove it for the finalizer callback so that
// we don't try to call it again on shutdown. // 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) { if (comptime IS_DEBUG) {
// should not be possible // should not be possible
std.debug.assert(false); std.debug.assert(false);
} }
return;
}; };
self.finalizer_callback_pool.destroy(fc.value);
return; return;
} }
@@ -1004,29 +1008,31 @@ pub fn queueMicrotaskFunc(self: *Context, cb: js.Function) void {
self.isolate.enqueueMicrotaskFunc(cb); 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 == // == Misc ==
// A type that has a finalizer can have its finalizer called one of two ways. // 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 // 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 // guaranteed to fire, so we track this in ctx._finalizers and call them on
// context shutdown. // context shutdown.
const FinalizerCallback = struct { pub const FinalizerCallback = struct {
ctx: *Context,
ptr: *anyopaque, ptr: *anyopaque,
global: v8.Global,
finalizerFn: *const fn (ptr: *anyopaque) void, finalizerFn: *const fn (ptr: *anyopaque) void,
pub fn init(ptr: anytype) FinalizerCallback { pub fn deinit(self: *FinalizerCallback) void {
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 {
self.finalizerFn(self.ptr); 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, .call_arena = page.call_arena,
.script_manager = &page._script_manager, .script_manager = &page._script_manager,
.scheduler = .init(context_arena), .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); 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. // context.global_objects, we want to track it in context.identity_map.
v8.v8__Global__New(isolate.handle, js_obj.handle, gop.value_ptr); v8.v8__Global__New(isolate.handle, js_obj.handle, gop.value_ptr);
if (@hasDecl(JsApi.Meta, "finalizer")) { if (@hasDecl(JsApi.Meta, "finalizer")) {
if (comptime IS_DEBUG) { // It would be great if resolved knew the resolved type, but I
// You can normally return a "*Node" and we'll correctly // can't figure out how to make that work, since it depends on
// handle it as what it really is, e.g. an HTMLScriptElement. // the [runtime] `value`.
// But for finalizers, we can't do that. I think this // We need the resolved finalizer, which we have in resolved.
// limitation will be OK - this auto-resolution is largely // The above if statement would be more clear as:
// limited to Node -> HtmlElement, none of which has finalizers // if (resolved.finalizer_from_v8) |finalizer| {
std.debug.assert(resolved.class_id == JsApi.Meta.class_id); // 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 (@hasDecl(JsApi.Meta, "weak")) {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
std.debug.assert(JsApi.Meta.weak == true); 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; 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 // This function recursively walks the _type union field (if there is one) to
// get the most specific class_id possible. // get the most specific class_id possible.
const Resolved = struct { const Resolved = struct {
weak: bool,
ptr: *anyopaque, ptr: *anyopaque,
class_id: u16, class_id: u16,
prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry, 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 { pub fn resolveValue(value: anytype) Resolved {
const T = bridge.Struct(@TypeOf(value)); const T = bridge.Struct(@TypeOf(value));
@@ -1056,13 +1066,28 @@ pub fn resolveValue(value: anytype) Resolved {
} }
fn resolveT(comptime T: type, value: *anyopaque) Resolved { fn resolveT(comptime T: type, value: *anyopaque) Resolved {
const Meta = T.JsApi.Meta;
return .{ return .{
.ptr = value, .ptr = value,
.class_id = T.JsApi.Meta.class_id, .class_id = Meta.class_id,
.prototype_chain = &T.JsApi.Meta.prototype_chain, .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 { pub fn stackTrace(self: *const Local) !?[]const u8 {
const isolate = self.isolate; const isolate = self.isolate;
const separator = log.separator(); const separator = log.separator();

View File

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

View File

@@ -76,7 +76,9 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, local: *const js.Local, page:
} }
// Dispatch abort event // 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( try page._event_manager.dispatchWithFunction(
self.asEventTarget(), self.asEventTarget(),
event, event,

View File

@@ -767,7 +767,8 @@ pub fn focus(self: *Element, page: *Page) !void {
return; 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); 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; 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); 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; page.document._active_element = null;
const Event = @import("Event.zig"); 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); 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 Node = @import("Node.zig");
const String = @import("../../string.zig").String; const String = @import("../../string.zig").String;
const Allocator = std.mem.Allocator;
pub const Event = @This(); pub const Event = @This();
pub const _prototype_root = true; pub const _prototype_root = true;
_type: Type, _type: Type,
_page: *Page,
_arena: Allocator,
_bubbles: bool = false, _bubbles: bool = false,
_cancelable: bool = false, _cancelable: bool = false,
_composed: bool = false, _composed: bool = false,
@@ -44,6 +47,12 @@ _time_stamp: u64,
_needs_retargeting: bool = false, _needs_retargeting: bool = false,
_isTrusted: 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) { pub const EventPhase = enum(u8) {
none = 0, none = 0,
capturing_phase = 1, capturing_phase = 1,
@@ -70,31 +79,38 @@ pub const Options = struct {
composed: bool = false, 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 { 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{}; const opts = opts_ orelse Options{};
// Round to 2ms for privacy (browsers do this) // Round to 2ms for privacy (browsers do this)
const raw_timestamp = @import("../../datetime.zig").milliTimestamp(.monotonic); const raw_timestamp = @import("../../datetime.zig").milliTimestamp(.monotonic);
const time_stamp = (raw_timestamp / 2) * 2; 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, ._type = .generic,
._bubbles = opts.bubbles, ._bubbles = opts.bubbles,
._time_stamp = time_stamp, ._time_stamp = time_stamp,
._cancelable = opts.cancelable, ._cancelable = opts.cancelable,
._composed = opts.composed, ._composed = opts.composed,
._type_string = try String.init(page.arena, typ, .{}), ._type_string = typ,
}); ._isTrusted = trusted,
};
event._isTrusted = trusted;
return event; return event;
} }
@@ -103,18 +119,22 @@ pub fn initEvent(
event_string: []const u8, event_string: []const u8,
bubbles: ?bool, bubbles: ?bool,
cancelable: ?bool, cancelable: ?bool,
page: *Page,
) !void { ) !void {
if (self._event_phase != .none) { if (self._event_phase != .none) {
return; 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._bubbles = bubbles orelse false;
self._cancelable = cancelable orelse false; self._cancelable = cancelable orelse false;
self._stop_propagation = 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 { pub fn as(self: *Event, comptime T: type) *T {
return self.is(T).?; return self.is(T).?;
} }
@@ -385,6 +405,8 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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, .{}); 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 (entry._url) |url| {
if (try page.isSameOrigin(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( try page._event_manager.dispatchWithFunction(
page.window.asEventTarget(), page.window.asEventTarget(),
event.asEvent(), event,
page.js.toLocal(page.window._on_popstate), page.js.toLocal(page.window._on_popstate),
.{ .context = "Pop State" }, .{ .context = "Pop State" },
); );

View File

@@ -122,14 +122,15 @@ const PostMessageCallback = struct {
return null; return null;
} }
const event = MessageEvent.initTrusted("message", .{ const event = (MessageEvent.initTrusted(comptime .wrap("message"), .{
.data = self.message, .data = self.message,
.origin = "", .origin = "",
.source = null, .source = null,
}, page) catch |err| { }, page) catch |err| {
log.err(.dom, "MessagePort.postMessage", .{ .err = err }); log.err(.dom, "MessagePort.postMessage", .{ .err = err });
return null; return null;
}; }).asEvent();
defer if (!event._v8_handoff) event.deinit(false);
var ls: js.Local.Scope = undefined; var ls: js.Local.Scope = undefined;
page.js.localScope(&ls); page.js.localScope(&ls);
@@ -137,7 +138,7 @@ const PostMessageCallback = struct {
page._event_manager.dispatchWithFunction( page._event_manager.dispatchWithFunction(
self.port.asEventTarget(), self.port.asEventTarget(),
event.asEvent(), event,
ls.toLocal(self.port._on_message), ls.toLocal(self.port._on_message),
.{ .context = "MessagePort message" }, .{ .context = "MessagePort message" },
) catch |err| { ) 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 { 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(), .@"error" = try err.persist(),
.message = err.toStringSlice() catch "Unknown error", .message = err.toStringSlice() catch "Unknown error",
.bubbles = false, .bubbles = false,
@@ -285,6 +285,7 @@ pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
}, page); }, page);
const event = error_event.asEvent(); 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 // Invoke window.onerror callback if set (per WHATWG spec, this is called
// with 5 arguments: message, source, lineno, colno, error) // 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; 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); try p._event_manager.dispatch(p.document.asEventTarget(), event);
pos.state = .end; pos.state = .end;
@@ -470,7 +472,8 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
.end => {}, .end => {},
.done => return null, .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); try p._event_manager.dispatch(p.document.asEventTarget(), event);
pos.state = .done; pos.state = .done;
@@ -640,15 +643,14 @@ const PostMessageCallback = struct {
const page = self.page; const page = self.page;
const window = page.window; const window = page.window;
const message_event = try MessageEvent.initTrusted("message", .{ const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{
.data = self.message, .data = self.message,
.origin = self.origin, .origin = self.origin,
.source = window, .source = window,
.bubbles = false, .bubbles = false,
.cancelable = false, .cancelable = false,
}, page); }, page)).asEvent();
defer if (!event._v8_handoff) event.deinit(false);
const event = message_event.asEvent();
try page._event_manager.dispatch(window.asEventTarget(), event); try page._event_manager.dispatch(window.asEventTarget(), event);
return null; return null;

View File

@@ -15,11 +15,13 @@
// //
// You should have received a copy of the GNU Affero General Public License // 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/>. // 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 js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Event = @import("../Event.zig"); const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator;
const CompositionEvent = @This(); const CompositionEvent = @This();
@@ -33,17 +35,28 @@ const CompositionEventOptions = struct {
const Options = Event.inheritOptions(CompositionEvent, CompositionEventOptions); const Options = Event.inheritOptions(CompositionEvent, CompositionEventOptions);
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CompositionEvent { 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{ const opts = opts_ orelse Options{};
._proto = undefined, const event = try page._factory.event(
._data = if (opts.data) |str| try page.dupeString(str) else "", arena,
}); type_string,
CompositionEvent{
._proto = undefined,
._data = if (opts.data) |str| try arena.dupe(u8, str) else "",
},
);
Event.populatePrototypes(event, opts, false); Event.populatePrototypes(event, opts, false);
return event; return event;
} }
pub fn deinit(self: *CompositionEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *CompositionEvent) *Event { pub fn asEvent(self: *CompositionEvent) *Event {
return self._proto; return self._proto;
} }
@@ -59,6 +72,8 @@ pub const JsApi = struct {
pub const name = "CompositionEvent"; pub const name = "CompositionEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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, .{}); 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> // Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io> // Pierre Tachoire <pierre@lightpanda.io>
@@ -17,9 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const js = @import("../../js/js.zig");
const String = @import("../../..//string.zig").String; const String = @import("../../..//string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Event = @import("../Event.zig"); const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@@ -37,11 +37,14 @@ const CustomEventOptions = struct {
const Options = Event.inheritOptions(CustomEvent, CustomEventOptions); const Options = Event.inheritOptions(CustomEvent, CustomEventOptions);
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CustomEvent { pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CustomEvent {
const arena = page.arena; const arena = try page.getArena(.{ .debug = "CustomEvent" });
const opts = opts_ orelse Options{}; errdefer page.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
const opts = opts_ orelse Options{};
const event = try page._factory.event( const event = try page._factory.event(
typ, arena,
type_string,
CustomEvent{ CustomEvent{
._arena = arena, ._arena = arena,
._proto = undefined, ._proto = undefined,
@@ -59,17 +62,20 @@ pub fn initCustomEvent(
bubbles: ?bool, bubbles: ?bool,
cancelable: ?bool, cancelable: ?bool,
detail_: ?js.Value.Global, detail_: ?js.Value.Global,
page: *Page,
) !void { ) !void {
// This function can only be called after the constructor has called. // This function can only be called after the constructor has called.
// So we assume proto is initialized already by constructor. // 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._bubbles = bubbles orelse false;
self._proto._cancelable = cancelable orelse false; self._proto._cancelable = cancelable orelse false;
// Detail is stored separately. // Detail is stored separately.
self._detail = detail_; self._detail = detail_;
} }
pub fn deinit(self: *CustomEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *CustomEvent) *Event { pub fn asEvent(self: *CustomEvent) *Event {
return self._proto; return self._proto;
} }
@@ -85,6 +91,8 @@ pub const JsApi = struct {
pub const name = "CustomEvent"; pub const name = "CustomEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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, .{}); 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> // Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io> // Pierre Tachoire <pierre@lightpanda.io>
@@ -17,9 +17,11 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); 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 Page = @import("../../Page.zig");
const Event = @import("../Event.zig"); const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@@ -44,18 +46,23 @@ pub const ErrorEventOptions = struct {
const Options = Event.inheritOptions(ErrorEvent, ErrorEventOptions); const Options = Event.inheritOptions(ErrorEvent, ErrorEventOptions);
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*ErrorEvent { 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 { pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*ErrorEvent {
return initWithTrusted(typ, opts_, true, page); 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 { fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, page: *Page) !*ErrorEvent {
const arena = page.arena;
const opts = opts_ orelse Options{}; const opts = opts_ orelse Options{};
const event = try page._factory.event( const event = try page._factory.event(
arena,
typ, typ,
ErrorEvent{ ErrorEvent{
._arena = arena, ._arena = arena,
@@ -72,6 +79,10 @@ fn initWithTrusted(typ: []const u8, opts_: ?Options, trusted: bool, page: *Page)
return event; return event;
} }
pub fn deinit(self: *ErrorEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *ErrorEvent) *Event { pub fn asEvent(self: *ErrorEvent) *Event {
return self._proto; return self._proto;
} }
@@ -103,6 +114,8 @@ pub const JsApi = struct {
pub const name = "ErrorEvent"; pub const name = "ErrorEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(ErrorEvent.deinit);
}; };
// Start API // Start API

View File

@@ -17,10 +17,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); 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 Event = @import("../Event.zig");
const UIEvent = @import("UIEvent.zig"); const UIEvent = @import("UIEvent.zig");
const Page = @import("../../Page.zig"); const Allocator = std.mem.Allocator;
const js = @import("../../js/js.zig");
const KeyboardEvent = @This(); const KeyboardEvent = @This();
@@ -180,24 +184,30 @@ const Options = Event.inheritOptions(
KeyboardEventOptions, KeyboardEventOptions,
); );
pub fn initTrusted(typ: []const u8, _opts: ?Options, page: *Page) !*KeyboardEvent { pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*KeyboardEvent {
return initWithTrusted(typ, _opts, true, page); 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 { 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 opts = _opts orelse Options{};
const event = try page._factory.uiEvent( const event = try page._factory.uiEvent(
arena,
typ, typ,
KeyboardEvent{ KeyboardEvent{
._proto = undefined, ._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, ._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, ._repeat = opts.repeat,
._is_composing = opts.isComposing, ._is_composing = opts.isComposing,
._ctrl_key = opts.ctrlKey, ._ctrl_key = opts.ctrlKey,
@@ -211,6 +221,10 @@ fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, page: *Page)
return event; return event;
} }
pub fn deinit(self: *KeyboardEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *KeyboardEvent) *Event { pub fn asEvent(self: *KeyboardEvent) *Event {
return self._proto.asEvent(); return self._proto.asEvent();
} }
@@ -251,8 +265,8 @@ pub fn getShiftKey(self: *const KeyboardEvent) bool {
return self._shift_key; return self._shift_key;
} }
pub fn getModifierState(self: *const KeyboardEvent, str: []const u8, page: *Page) !bool { pub fn getModifierState(self: *const KeyboardEvent, str: []const u8) !bool {
const key = try Key.fromString(page.arena, str); const key = try Key.fromString(self._proto._proto._arena, str);
switch (key) { switch (key) {
.Alt, .AltGraph => return self._alt_key, .Alt, .AltGraph => return self._alt_key,
@@ -274,6 +288,8 @@ pub const JsApi = struct {
pub const name = "KeyboardEvent"; pub const name = "KeyboardEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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, .{}); 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> // Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io> // Pierre Tachoire <pierre@lightpanda.io>
@@ -16,11 +16,15 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // 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 js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Event = @import("../Event.zig"); const Event = @import("../Event.zig");
const Window = @import("../Window.zig"); const Window = @import("../Window.zig");
const Allocator = std.mem.Allocator;
const MessageEvent = @This(); const MessageEvent = @This();
@@ -38,22 +42,28 @@ const MessageEventOptions = struct {
const Options = Event.inheritOptions(MessageEvent, MessageEventOptions); const Options = Event.inheritOptions(MessageEvent, MessageEventOptions);
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*MessageEvent { 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 { pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*MessageEvent {
return initWithTrusted(typ, opts_, true, page); 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 opts = opts_ orelse Options{};
const event = try page._factory.event( const event = try page._factory.event(
arena,
typ, typ,
MessageEvent{ MessageEvent{
._proto = undefined, ._proto = undefined,
._data = opts.data, ._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, ._source = opts.source,
}, },
); );
@@ -62,6 +72,10 @@ fn initWithTrusted(typ: []const u8, opts_: ?Options, trusted: bool, page: *Page)
return event; return event;
} }
pub fn deinit(self: *MessageEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *MessageEvent) *Event { pub fn asEvent(self: *MessageEvent) *Event {
return self._proto; return self._proto;
} }
@@ -85,6 +99,8 @@ pub const JsApi = struct {
pub const name = "MessageEvent"; pub const name = "MessageEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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, .{}); 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Event = @import("../Event.zig"); const String = @import("../../../string.zig").String;
const UIEvent = @import("UIEvent.zig");
const EventTarget = @import("../EventTarget.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const js = @import("../../js/js.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 PointerEvent = @import("PointerEvent.zig");
const MouseEvent = @This(); const MouseEvent = @This();
@@ -75,10 +78,15 @@ pub const Options = Event.inheritOptions(
); );
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*MouseEvent { 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 opts = _opts orelse Options{};
const event = try page._factory.uiEvent( const event = try page._factory.uiEvent(
typ, arena,
type_string,
MouseEvent{ MouseEvent{
._type = .generic, ._type = .generic,
._proto = undefined, ._proto = undefined,
@@ -99,6 +107,10 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*MouseEvent {
return event; return event;
} }
pub fn deinit(self: *MouseEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *MouseEvent) *Event { pub fn asEvent(self: *MouseEvent) *Event {
return self._proto.asEvent(); return self._proto.asEvent();
} }
@@ -172,6 +184,8 @@ pub const JsApi = struct {
pub const name = "MouseEvent"; pub const name = "MouseEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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, .{}); 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); 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 Page = @import("../../Page.zig");
const Event = @import("../Event.zig");
const NavigationHistoryEntry = @import("../navigation/NavigationHistoryEntry.zig"); const NavigationHistoryEntry = @import("../navigation/NavigationHistoryEntry.zig");
const NavigationType = @import("../navigation/root.zig").NavigationType; const NavigationType = @import("../navigation/root.zig").NavigationType;
const js = @import("../../js/js.zig"); const Allocator = std.mem.Allocator;
const NavigationCurrentEntryChangeEvent = @This(); const NavigationCurrentEntryChangeEvent = @This();
@@ -40,15 +44,21 @@ const Options = Event.inheritOptions(
); );
pub fn init(typ: []const u8, opts: Options, page: *Page) !*NavigationCurrentEntryChangeEvent { 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 { pub fn initTrusted(typ: String, opts: Options, page: *Page) !*NavigationCurrentEntryChangeEvent {
return initWithTrusted(typ, opts, true, page); const arena = try page.getArena(.{ .debug = "NavigationCurrentEntryChangeEvent.trusted" });
errdefer page.releaseArena(arena);
return initWithTrusted(arena, typ, opts, true, page);
} }
fn initWithTrusted( fn initWithTrusted(
typ: []const u8, arena: Allocator,
typ: String,
opts: Options, opts: Options,
trusted: bool, trusted: bool,
page: *Page, page: *Page,
@@ -59,6 +69,7 @@ fn initWithTrusted(
null; null;
const event = try page._factory.event( const event = try page._factory.event(
arena,
typ, typ,
NavigationCurrentEntryChangeEvent{ NavigationCurrentEntryChangeEvent{
._proto = undefined, ._proto = undefined,
@@ -71,6 +82,10 @@ fn initWithTrusted(
return event; return event;
} }
pub fn deinit(self: *NavigationCurrentEntryChangeEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *NavigationCurrentEntryChangeEvent) *Event { pub fn asEvent(self: *NavigationCurrentEntryChangeEvent) *Event {
return self._proto; return self._proto;
} }
@@ -90,6 +105,8 @@ pub const JsApi = struct {
pub const name = "NavigationCurrentEntryChangeEvent"; pub const name = "NavigationCurrentEntryChangeEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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, .{}); 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> // Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io> // Pierre Tachoire <pierre@lightpanda.io>
@@ -16,9 +16,13 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // 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 js = @import("../../js/js.zig");
const Page = @import("../../Page.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 // https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent
const PageTransitionEvent = @This(); const PageTransitionEvent = @This();
@@ -33,17 +37,23 @@ const PageTransitionEventOptions = struct {
const Options = Event.inheritOptions(PageTransitionEvent, PageTransitionEventOptions); const Options = Event.inheritOptions(PageTransitionEvent, PageTransitionEventOptions);
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PageTransitionEvent { 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 { pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*PageTransitionEvent {
return initWithTrusted(typ, _opts, true, page); 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 opts = _opts orelse Options{};
const event = try page._factory.event( const event = try page._factory.event(
arena,
typ, typ,
PageTransitionEvent{ PageTransitionEvent{
._proto = undefined, ._proto = undefined,
@@ -55,6 +65,10 @@ fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, page: *Page)
return event; return event;
} }
pub fn deinit(self: *PageTransitionEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *PageTransitionEvent) *Event { pub fn asEvent(self: *PageTransitionEvent) *Event {
return self._proto; return self._proto;
} }
@@ -70,6 +84,8 @@ pub const JsApi = struct {
pub const name = "PageTransitionEvent"; pub const name = "PageTransitionEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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, .{}); 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Event = @import("../Event.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 { 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( const event = try page._factory.mouseEvent(
typ, arena,
type_string,
MouseEvent{ MouseEvent{
._type = .{ .pointer_event = undefined }, ._type = .{ .pointer_event = undefined },
._proto = undefined, ._proto = undefined,
@@ -120,6 +126,10 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PointerEvent {
return event; return event;
} }
pub fn deinit(self: *PointerEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *PointerEvent) *Event { pub fn asEvent(self: *PointerEvent) *Event {
return self._proto.asEvent(); return self._proto.asEvent();
} }
@@ -179,6 +189,8 @@ pub const JsApi = struct {
pub const name = "PointerEvent"; pub const name = "PointerEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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, .{}); 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> // Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io> // Pierre Tachoire <pierre@lightpanda.io>
@@ -16,10 +16,15 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // 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 js = @import("../../js/js.zig");
const Page = @import("../../Page.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 // https://developer.mozilla.org/en-US/docs/Web/API/PopStateEvent
const PopStateEvent = @This(); const PopStateEvent = @This();
@@ -33,17 +38,23 @@ const PopStateEventOptions = struct {
const Options = Event.inheritOptions(PopStateEvent, PopStateEventOptions); const Options = Event.inheritOptions(PopStateEvent, PopStateEventOptions);
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PopStateEvent { 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 { pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*PopStateEvent {
return initWithTrusted(typ, _opts, true, page); 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 opts = _opts orelse Options{};
const event = try page._factory.event( const event = try page._factory.event(
arena,
typ, typ,
PopStateEvent{ PopStateEvent{
._proto = undefined, ._proto = undefined,
@@ -55,6 +66,10 @@ fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, page: *Page)
return event; return event;
} }
pub fn deinit(self: *PopStateEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *PopStateEvent) *Event { pub fn asEvent(self: *PopStateEvent) *Event {
return self._proto; return self._proto;
} }
@@ -76,6 +91,8 @@ pub const JsApi = struct {
pub const name = "PopStateEvent"; pub const name = "PopStateEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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, .{}); 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 // 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/>. // 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 Page = @import("../../Page.zig");
const Event = @import("../Event.zig"); const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator;
const ProgressEvent = @This(); const ProgressEvent = @This();
_proto: *Event, _proto: *Event,
@@ -34,17 +38,23 @@ const ProgressEventOptions = struct {
const Options = Event.inheritOptions(ProgressEvent, ProgressEventOptions); const Options = Event.inheritOptions(ProgressEvent, ProgressEventOptions);
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*ProgressEvent { 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 { pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*ProgressEvent {
return initWithTrusted(typ, _opts, true, page); 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 opts = _opts orelse Options{};
const event = try page._factory.event( const event = try page._factory.event(
arena,
typ, typ,
ProgressEvent{ ProgressEvent{
._proto = undefined, ._proto = undefined,
@@ -57,6 +67,10 @@ fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, page: *Page)
return event; return event;
} }
pub fn deinit(self: *ProgressEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn asEvent(self: *ProgressEvent) *Event { pub fn asEvent(self: *ProgressEvent) *Event {
return self._proto; return self._proto;
} }
@@ -81,6 +95,8 @@ pub const JsApi = struct {
pub const name = "ProgressEvent"; pub const name = "ProgressEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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, .{}); 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 // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const Event = @import("../Event.zig"); const String = @import("../../../string.zig").String;
const Window = @import("../Window.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Event = @import("../Event.zig");
const Window = @import("../Window.zig");
const UIEvent = @This(); const UIEvent = @This();
_type: Type, _type: Type,
@@ -45,10 +47,14 @@ pub const Options = Event.inheritOptions(
); );
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*UIEvent { 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( const event = try page._factory.event(
typ, arena,
type_string,
UIEvent{ UIEvent{
._type = .generic, ._type = .generic,
._proto = undefined, ._proto = undefined,
@@ -61,6 +67,10 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*UIEvent {
return event; return event;
} }
pub fn deinit(self: *UIEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
}
pub fn as(self: *UIEvent, comptime T: type) *T { pub fn as(self: *UIEvent, comptime T: type) *T {
return self.is(T).?; return self.is(T).?;
} }
@@ -105,6 +115,8 @@ pub const JsApi = struct {
pub const name = "UIEvent"; pub const name = "UIEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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, .{}); pub const constructor = bridge.constructor(UIEvent.init, .{});

View File

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

View File

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

View File

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

View File

@@ -61,7 +61,7 @@ fn dispatchKeyEvent(cmd: anytype) !void {
const page = bc.session.currentPage() orelse return; const page = bc.session.currentPage() orelse return;
const KeyboardEvent = @import("../../browser/webapi/event/KeyboardEvent.zig"); 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, .key = params.key,
.code = params.code, .code = params.code,
.altKey = params.modifiers & 1 == 1, .altKey = params.modifiers & 1 == 1,

View File

@@ -31,7 +31,7 @@ pub fn main() !void {
// allocator // allocator
// - in Debug mode we use the General Purpose Allocator to detect memory leaks // - in Debug mode we use the General Purpose Allocator to detect memory leaks
// - in Release mode we use the c allocator // - 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; const gpa = if (builtin.mode == .Debug) gpa_instance.allocator() else std.heap.c_allocator;
defer if (builtin.mode == .Debug) { defer if (builtin.mode == .Debug) {