diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index ba4e3ddd..274406b7 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -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(); diff --git a/src/browser/Page.zig b/src/browser/Page.zig index a9462d1e..e2a81a2e 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -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; diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 5e05935e..f87853b8 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -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 { diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 33480c59..7fc9073f 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -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); } }; diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 7aa57410..057c2c87 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -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); diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig index 2d20e7ab..d689af2f 100644 --- a/src/browser/js/Local.zig +++ b/src/browser/js/Local.zig @@ -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(); diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 2b12d9a8..5adb5eac 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -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, }; diff --git a/src/browser/webapi/AbortSignal.zig b/src/browser/webapi/AbortSignal.zig index e5039b28..6472299c 100644 --- a/src/browser/webapi/AbortSignal.zig +++ b/src/browser/webapi/AbortSignal.zig @@ -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, diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index fe165eaf..2e2d4194 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -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); } diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 8e7f4010..721a032d 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -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, .{}); diff --git a/src/browser/webapi/History.zig b/src/browser/webapi/History.zig index 825b9464..cf665e05 100644 --- a/src/browser/webapi/History.zig +++ b/src/browser/webapi/History.zig @@ -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" }, ); diff --git a/src/browser/webapi/MessagePort.zig b/src/browser/webapi/MessagePort.zig index 58dd699c..448df566 100644 --- a/src/browser/webapi/MessagePort.zig +++ b/src/browser/webapi/MessagePort.zig @@ -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| { diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index c5dd3c1c..6e05be6a 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -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; diff --git a/src/browser/webapi/event/CompositionEvent.zig b/src/browser/webapi/event/CompositionEvent.zig index f77489dc..d2543d73 100644 --- a/src/browser/webapi/event/CompositionEvent.zig +++ b/src/browser/webapi/event/CompositionEvent.zig @@ -15,11 +15,13 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +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, .{}); diff --git a/src/browser/webapi/event/CustomEvent.zig b/src/browser/webapi/event/CustomEvent.zig index be5820d0..029827b0 100644 --- a/src/browser/webapi/event/CustomEvent.zig +++ b/src/browser/webapi/event/CustomEvent.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire @@ -17,9 +17,9 @@ // along with this program. If not, see . 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, .{}); diff --git a/src/browser/webapi/event/ErrorEvent.zig b/src/browser/webapi/event/ErrorEvent.zig index 296def3c..ce8360d1 100644 --- a/src/browser/webapi/event/ErrorEvent.zig +++ b/src/browser/webapi/event/ErrorEvent.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire @@ -17,9 +17,11 @@ // along with this program. If not, see . 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 diff --git a/src/browser/webapi/event/KeyboardEvent.zig b/src/browser/webapi/event/KeyboardEvent.zig index f175ddee..7f062840 100644 --- a/src/browser/webapi/event/KeyboardEvent.zig +++ b/src/browser/webapi/event/KeyboardEvent.zig @@ -17,10 +17,14 @@ // along with this program. If not, see . 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, .{}); diff --git a/src/browser/webapi/event/MessageEvent.zig b/src/browser/webapi/event/MessageEvent.zig index bdf89fbd..45d24b9e 100644 --- a/src/browser/webapi/event/MessageEvent.zig +++ b/src/browser/webapi/event/MessageEvent.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire @@ -16,11 +16,15 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +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, .{}); diff --git a/src/browser/webapi/event/MouseEvent.zig b/src/browser/webapi/event/MouseEvent.zig index c2c1c153..f2eac422 100644 --- a/src/browser/webapi/event/MouseEvent.zig +++ b/src/browser/webapi/event/MouseEvent.zig @@ -17,11 +17,14 @@ // along with this program. If not, see . 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, .{}); diff --git a/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig b/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig index 0a12a8a3..47101f2a 100644 --- a/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig +++ b/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig @@ -17,11 +17,15 @@ // along with this program. If not, see . 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, .{}); diff --git a/src/browser/webapi/event/PageTransitionEvent.zig b/src/browser/webapi/event/PageTransitionEvent.zig index 4a260481..259c68db 100644 --- a/src/browser/webapi/event/PageTransitionEvent.zig +++ b/src/browser/webapi/event/PageTransitionEvent.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire @@ -16,9 +16,13 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -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, .{}); diff --git a/src/browser/webapi/event/PointerEvent.zig b/src/browser/webapi/event/PointerEvent.zig index fab647ba..97f03eb8 100644 --- a/src/browser/webapi/event/PointerEvent.zig +++ b/src/browser/webapi/event/PointerEvent.zig @@ -17,6 +17,8 @@ // along with this program. If not, see . 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, .{}); diff --git a/src/browser/webapi/event/PopStateEvent.zig b/src/browser/webapi/event/PopStateEvent.zig index 78480402..d346d9cc 100644 --- a/src/browser/webapi/event/PopStateEvent.zig +++ b/src/browser/webapi/event/PopStateEvent.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire @@ -16,10 +16,15 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -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, .{}); diff --git a/src/browser/webapi/event/ProgressEvent.zig b/src/browser/webapi/event/ProgressEvent.zig index 61fe49c9..8e061c71 100644 --- a/src/browser/webapi/event/ProgressEvent.zig +++ b/src/browser/webapi/event/ProgressEvent.zig @@ -16,8 +16,12 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +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, .{}); diff --git a/src/browser/webapi/event/UIEvent.zig b/src/browser/webapi/event/UIEvent.zig index 8ad24858..6e50603e 100644 --- a/src/browser/webapi/event/UIEvent.zig +++ b/src/browser/webapi/event/UIEvent.zig @@ -16,11 +16,13 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -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, .{}); diff --git a/src/browser/webapi/navigation/Navigation.zig b/src/browser/webapi/navigation/Navigation.zig index 14a11183..6ecbad0c 100644 --- a/src/browser/webapi/navigation/Navigation.zig +++ b/src/browser/webapi/navigation/Navigation.zig @@ -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, ); diff --git a/src/browser/webapi/navigation/NavigationEventTarget.zig b/src/browser/webapi/navigation/NavigationEventTarget.zig index c89831d8..51a3f54d 100644 --- a/src/browser/webapi/navigation/NavigationEventTarget.zig +++ b/src/browser/webapi/navigation/NavigationEventTarget.zig @@ -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) { diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index ec2a2d16..0bff9296 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -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, diff --git a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig index 6b8abb19..355eddda 100644 --- a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig +++ b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig @@ -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 }, ); diff --git a/src/cdp/domains/input.zig b/src/cdp/domains/input.zig index 1cd65085..464a4feb 100644 --- a/src/cdp/domains/input.zig +++ b/src/cdp/domains/input.zig @@ -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, diff --git a/src/main.zig b/src/main.zig index 106ed757..3bbcb492 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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) {