Unify the Zig and JS events using an intrusive node.

The approach borrows heavily from Zig's new LinkedList API.

The main benefit is that it unifies how event callbacks are done. When the
Page.windowClick event was added, the Event structure was changed to a union,
supporting a distinct Zig and JS event.

This new approach more or less treats everything like a Zig event. A JS event
is just a Zig struct that has a Env.Callback which it can invoke in its handle
method.

The intrusive nature of the EventNode means that what used to be 1 or 2
allocations is now 0 or 1.

It also has the benefit of making netsurf completely unaware of Env.Callbacks.
This commit is contained in:
Karl Seguin
2025-04-26 22:17:29 +08:00
parent 0fb0532875
commit 072110481f
7 changed files with 142 additions and 220 deletions

View File

@@ -341,12 +341,15 @@ pub const Page = struct {
renderer: FlatRenderer, renderer: FlatRenderer,
window_clicked_event_node: parser.EventNode,
fn init(session: *Session) Page { fn init(session: *Session) Page {
const arena = session.browser.page_arena.allocator(); const arena = session.browser.page_arena.allocator();
return .{ return .{
.arena = arena, .arena = arena,
.session = session, .session = session,
.renderer = FlatRenderer.init(arena), .renderer = FlatRenderer.init(arena),
.window_clicked_event_node = .{ .func = windowClicked },
}; };
} }
@@ -481,12 +484,10 @@ pub const Page = struct {
self.doc = doc; self.doc = doc;
const document_element = (try parser.documentGetDocumentElement(doc)) orelse return error.DocumentElementError; const document_element = (try parser.documentGetDocumentElement(doc)) orelse return error.DocumentElementError;
try parser.eventTargetAddZigListener( try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Element, document_element), parser.toEventTarget(parser.Element, document_element),
arena,
"click", "click",
windowClicked, &self.window_clicked_event_node,
self,
false, false,
); );
@@ -766,8 +767,8 @@ pub const Page = struct {
_ = try parser.elementDispatchEvent(element, @ptrCast(event)); _ = try parser.elementDispatchEvent(element, @ptrCast(event));
} }
fn windowClicked(ctx: *anyopaque, event: *parser.Event) void { fn windowClicked(node: *parser.EventNode, event: *parser.Event) void {
const self: *Page = @alignCast(@ptrCast(ctx)); const self: *Page = @fieldParentPtr("window_clicked_event_node", node);
self._windowClicked(event) catch |err| { self._windowClicked(event) catch |err| {
log.err("window click handler: {}", .{err}); log.err("window click handler: {}", .{err});
}; };

View File

@@ -46,7 +46,7 @@ pub const EventTarget = struct {
pub fn _addEventListener( pub fn _addEventListener(
self: *parser.EventTarget, self: *parser.EventTarget,
eventType: []const u8, typ: []const u8,
cbk: Env.Callback, cbk: Env.Callback,
capture: ?bool, capture: ?bool,
state: *SessionState, state: *SessionState,
@@ -56,7 +56,7 @@ pub const EventTarget = struct {
// check if event target has already this listener // check if event target has already this listener
const lst = try parser.eventTargetHasListener( const lst = try parser.eventTargetHasListener(
self, self,
eventType, typ,
capture orelse false, capture orelse false,
cbk.id, cbk.id,
); );
@@ -64,29 +64,28 @@ pub const EventTarget = struct {
return; return;
} }
const eh = try EventHandler.init(state.arena, try cbk.withThis(self));
try parser.eventTargetAddEventListener( try parser.eventTargetAddEventListener(
self, self,
state.arena, typ,
eventType, &eh.node,
EventHandler,
.{ .cbk = cbk },
capture orelse false, capture orelse false,
); );
} }
pub fn _removeEventListener( pub fn _removeEventListener(
self: *parser.EventTarget, self: *parser.EventTarget,
eventType: []const u8, typ: []const u8,
cbk: Env.Callback, cbk: Env.Callback,
capture: ?bool, capture: ?bool,
state: *SessionState,
// TODO: hanle EventListenerOptions // TODO: hanle EventListenerOptions
// see #https://github.com/lightpanda-io/jsruntime-lib/issues/114 // see #https://github.com/lightpanda-io/jsruntime-lib/issues/114
) !void { ) !void {
// check if event target has already this listener // check if event target has already this listener
const lst = try parser.eventTargetHasListener( const lst = try parser.eventTargetHasListener(
self, self,
eventType, typ,
capture orelse false, capture orelse false,
cbk.id, cbk.id,
); );
@@ -97,8 +96,7 @@ pub const EventTarget = struct {
// remove listener // remove listener
try parser.eventTargetRemoveEventListener( try parser.eventTargetRemoveEventListener(
self, self,
state.arena, typ,
eventType,
lst.?, lst.?,
capture orelse false, capture orelse false,
); );

View File

@@ -59,56 +59,45 @@ pub const MutationObserver = struct {
.node = node, .node = node,
.options = options, .options = options,
.mutation_observer = self, .mutation_observer = self,
.event_node = .{ .id = self.cbk.id, .func = Observer.handle },
}; };
const arena = self.arena;
// register node's events // register node's events
if (options.childList or options.subtree) { if (options.childList or options.subtree) {
try parser.eventTargetAddZigListener( try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
arena,
"DOMNodeInserted", "DOMNodeInserted",
Observer.handle, &observer.event_node,
observer,
false, false,
); );
try parser.eventTargetAddZigListener( try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
arena,
"DOMNodeRemoved", "DOMNodeRemoved",
Observer.handle, &observer.event_node,
observer,
false, false,
); );
} }
if (options.attr()) { if (options.attr()) {
try parser.eventTargetAddZigListener( try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
arena,
"DOMAttrModified", "DOMAttrModified",
Observer.handle, &observer.event_node,
observer,
false, false,
); );
} }
if (options.cdata()) { if (options.cdata()) {
try parser.eventTargetAddZigListener( try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
arena,
"DOMCharacterDataModified", "DOMCharacterDataModified",
Observer.handle, &observer.event_node,
observer,
false, false,
); );
} }
if (options.subtree) { if (options.subtree) {
try parser.eventTargetAddZigListener( try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
arena,
"DOMSubtreeModified", "DOMSubtreeModified",
Observer.handle, &observer.event_node,
observer,
false, false,
); );
} }
@@ -221,6 +210,8 @@ const Observer = struct {
// and batch the mutation records. // and batch the mutation records.
mutation_observer: *MutationObserver, mutation_observer: *MutationObserver,
event_node: parser.EventNode,
fn appliesTo(o: *const Observer, target: *parser.Node) bool { fn appliesTo(o: *const Observer, target: *parser.Node) bool {
// mutation on any target is always ok. // mutation on any target is always ok.
if (o.options.subtree) { if (o.options.subtree) {
@@ -250,9 +241,9 @@ const Observer = struct {
return false; return false;
} }
fn handle(ctx: *anyopaque, event: *parser.Event) void { fn handle(en: *parser.EventNode, event: *parser.Event) void {
// retrieve the observer from the data. const self: *Observer = @fieldParentPtr("event_node", en);
var self: *Observer = @alignCast(@ptrCast(ctx));
var mutation_observer = self.mutation_observer; var mutation_observer = self.mutation_observer;
const node = blk: { const node = blk: {

View File

@@ -17,6 +17,7 @@
// 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 Allocator = std.mem.Allocator;
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Callback = @import("../env.zig").Callback; const Callback = @import("../env.zig").Callback;
@@ -136,14 +137,35 @@ pub const Event = struct {
}; };
pub const EventHandler = struct { pub const EventHandler = struct {
fn handle(event: ?*parser.Event, data: *const parser.JSEventHandlerData) void { callback: Callback,
node: parser.EventNode,
pub fn init(allocator: Allocator, callback: Callback) !*EventHandler {
const eh = try allocator.create(EventHandler);
eh.* = .{
.callback = callback,
.node = .{
.id = callback.id,
.func = handle,
},
};
return eh;
}
fn handle(node: *parser.EventNode, event: *parser.Event) void {
const ievent = Event.toInterface(event) catch |err| {
log.err("Event.toInterface: {}", .{err});
return;
};
const self: *EventHandler = @fieldParentPtr("node", node);
var result: Callback.Result = undefined; var result: Callback.Result = undefined;
data.cbk.tryCall(.{if (event) |evt| Event.toInterface(evt) catch unreachable else null}, &result) catch { self.callback.tryCall(.{ievent}, &result) catch {
log.err("event handler error: {s}", .{result.exception}); log.err("event handler error: {s}", .{result.exception});
log.debug("stack:\n{s}", .{result.stack orelse "???"}); log.debug("stack:\n{s}", .{result.stack orelse "???"});
}; };
} }
}.handle; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "Browser.Event" { test "Browser.Event" {

View File

@@ -29,9 +29,6 @@ const c = @cImport({
const mimalloc = @import("mimalloc.zig"); const mimalloc = @import("mimalloc.zig");
const Callback = @import("env.zig").Callback;
const SessionState = @import("env.zig").SessionState;
// init initializes netsurf lib. // init initializes netsurf lib.
// init starts a mimalloc heap arena for the netsurf session. The caller must // init starts a mimalloc heap arena for the netsurf session. The caller must
// call deinit() to free the arena memory. // call deinit() to free the arena memory.
@@ -587,11 +584,61 @@ pub inline fn toEventTarget(comptime T: type, v: *T) *EventTarget {
return @as(*EventTarget, @ptrCast(et_aligned)); return @as(*EventTarget, @ptrCast(et_aligned));
} }
// The way we implement events is a lot like how Zig implements linked lists.
// A Zig struct contains an `EventNode` field, i.e.:
// node: parser.EventNode,
//
// When eventTargetAddEventListener is called, we pass in `&self.node`.
// This is the pointer that's stored in the netsurf listener and it's the data
// we can get back from the listener. We can call the node's `func` function,
// passing the node itself, and the receiving function will know how to turn
// that node into the our "self", i..e by using @fieldParentPtr.
// https://www.openmymind.net/Zigs-New-LinkedList-API/
pub const EventNode = struct {
// Event id, used for removing. Internal Zig events won't have an id.
// This is normally set to the callback.id for a JavaScript event.
id: ?usize = null,
func: *const fn (node: *EventNode, event: *Event) void,
fn idFromListener(lst: *EventListener) ?usize {
const ctx = eventListenerGetData(lst) orelse return null;
const node: *EventNode = @alignCast(@ptrCast(ctx));
return node.id;
}
};
pub fn eventTargetAddEventListener(
et: *EventTarget,
typ: []const u8,
node: *EventNode,
capture: bool,
) !void {
const event_handler = struct {
fn handle(event_: ?*Event, ptr_: ?*anyopaque) callconv(.C) void {
const ptr = ptr_ orelse return;
const event = event_ orelse return;
const node_: *EventNode = @alignCast(@ptrCast(ptr));
node_.func(node_, event);
}
}.handle;
var listener: ?*EventListener = undefined;
const errLst = c.dom_event_listener_create(event_handler, node, &listener);
try DOMErr(errLst);
defer c.dom_event_listener_unref(listener);
const s = try strFromData(typ);
const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, capture);
try DOMErr(err);
}
pub fn eventTargetHasListener( pub fn eventTargetHasListener(
et: *EventTarget, et: *EventTarget,
typ: []const u8, typ: []const u8,
capture: bool, capture: bool,
cbk_id: usize, id: usize,
) !?*EventListener { ) !?*EventListener {
const str = try strFromData(typ); const str = try strFromData(typ);
@@ -616,12 +663,9 @@ pub fn eventTargetHasListener(
// and capture property, // and capture property,
// let's check if the callback handler is the same // let's check if the callback handler is the same
defer c.dom_event_listener_unref(listener); defer c.dom_event_listener_unref(listener);
if (EventHandlerData.fromListener(listener)) |ehd| { if (EventNode.idFromListener(listener)) |node_id| {
switch (ehd.*) { if (node_id == id) {
.js => |js| if (cbk_id == js.data.cbk.id) { return lst;
return lst;
},
.zig => {},
} }
} }
} }
@@ -638,144 +682,18 @@ pub fn eventTargetHasListener(
return null; return null;
} }
// The *anyopque that get stored in the libdom listener, which we'll retrieve
// when then event is dispatched so that we can execute the JS or Zig callback.
const EventHandlerData = union(enum) {
js: JS,
zig: Zig,
const JS = struct {
data: JSEventHandlerData,
func: JSEventHandlerFunc,
};
const Zig = struct {
ctx: *anyopaque,
func: ZigEventHandlerFunc,
};
// retrieve a EventHandlerDataInternal from a listener.
fn fromListener(lst: *EventListener) ?*EventHandlerData {
const ctx = eventListenerGetData(lst) orelse return null;
const ehd: *EventHandlerData = @alignCast(@ptrCast(ctx));
return ehd;
}
pub fn deinit(self: *EventHandlerData, alloc: std.mem.Allocator) void {
switch (self.*) {
.js => |*js| {
const js_data = &js.data;
if (js_data.deinitFunc) |df| {
df(js_data.ctx, alloc);
}
},
.zig => {},
}
alloc.destroy(self);
}
pub fn handle(self: *EventHandlerData, event: ?*Event) void {
switch (self.*) {
.js => |*js| js.func(event, &js.data),
.zig => |zig| zig.func(zig.ctx, event.?),
}
}
};
pub const JSEventHandlerData = struct {
cbk: Callback,
ctx: ?*anyopaque = null,
// deinitFunc implements the data deinitialization.
deinitFunc: ?DeinitFunc = null,
pub const DeinitFunc = *const fn (data: ?*anyopaque, alloc: std.mem.Allocator) void;
};
const JSEventHandlerFunc = *const fn (event: ?*Event, data: *JSEventHandlerData) void;
const ZigEventHandlerFunc = *const fn (ctx: *anyopaque, event: *Event) void;
pub fn eventTargetAddEventListener(
et: *EventTarget,
alloc: std.mem.Allocator,
typ: []const u8,
func: JSEventHandlerFunc,
data: JSEventHandlerData,
capture: bool,
) !void {
// this allocation will be removed either on
// eventTargetRemoveEventListener or eventTargetRemoveAllEventListeners
const ehd = try alloc.create(EventHandlerData);
errdefer alloc.destroy(ehd);
ehd.* = .{ .js = .{ .data = data, .func = func } };
errdefer ehd.deinit(alloc);
// When a function is used as an event handler, its this parameter is bound
// to the DOM element on which the listener is placed.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#this_in_dom_event_handlers
try ehd.js.data.cbk.setThis(et);
return addEventTargetListener(et, typ, ehd, capture);
}
pub fn eventTargetAddZigListener(
et: *EventTarget,
alloc: std.mem.Allocator,
typ: []const u8,
func: ZigEventHandlerFunc,
ctx: *anyopaque,
capture: bool,
) !void {
const ehd = try alloc.create(EventHandlerData);
errdefer alloc.destroy(ehd);
ehd.* = .{ .zig = .{ .ctx = ctx, .func = func } };
return addEventTargetListener(et, typ, ehd, capture);
}
fn addEventTargetListener(et: *EventTarget, typ: []const u8, data: *anyopaque, capture: bool) !void {
// event_handler implements the function exposed in C and called by libdom.
// It retrieves the EventHandler and calls the appropriate (JS or Zig)
// handler function with the corresponding data.
const event_handler = struct {
fn handle(event: ?*Event, ptr_: ?*anyopaque) callconv(.C) void {
const ptr = ptr_ orelse return;
@as(*EventHandlerData, @alignCast(@ptrCast(ptr))).handle(event);
// NOTE: we can not call func.deinit here
// b/c the handler can be called several times
// either on this dispatch event or in anoter one
}
}.handle;
var listener: ?*EventListener = undefined;
const errLst = c.dom_event_listener_create(event_handler, data, &listener);
try DOMErr(errLst);
defer c.dom_event_listener_unref(listener);
const s = try strFromData(typ);
const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, capture);
try DOMErr(err);
}
pub fn eventTargetRemoveEventListener( pub fn eventTargetRemoveEventListener(
et: *EventTarget, et: *EventTarget,
alloc: std.mem.Allocator,
typ: []const u8, typ: []const u8,
lst: *EventListener, lst: *EventListener,
capture: bool, capture: bool,
) !void { ) !void {
// free data allocation made on eventTargetAddEventListener
if (EventHandlerData.fromListener(lst)) |ehd| {
ehd.deinit(alloc);
}
const s = try strFromData(typ); const s = try strFromData(typ);
const err = eventTargetVtable(et).remove_event_listener.?(et, s, lst, capture); const err = eventTargetVtable(et).remove_event_listener.?(et, s, lst, capture);
try DOMErr(err); try DOMErr(err);
} }
pub fn eventTargetRemoveAllEventListeners( pub fn eventTargetRemoveAllEventListeners(et: *EventTarget) !void {
et: *EventTarget,
alloc: std.mem.Allocator,
) !void {
var next: ?*EventListenerEntry = undefined; var next: ?*EventListenerEntry = undefined;
var lst: ?*EventListener = undefined; var lst: ?*EventListener = undefined;
@@ -792,15 +710,8 @@ pub fn eventTargetRemoveAllEventListeners(
try DOMErr(errIter); try DOMErr(errIter);
if (lst) |listener| { if (lst) |listener| {
defer c.dom_event_listener_unref(listener); if (EventNode.idFromListener(listener) != null) {
defer c.dom_event_listener_unref(listener);
if (EventHandlerData.fromListener(listener)) |ehd| {
if (ehd.* == .zig) {
// we don't remove Zig listeners
continue;
}
ehd.deinit(alloc);
const err = eventTargetVtable(et).remove_event_listener.?( const err = eventTargetVtable(et).remove_event_listener.?(
et, et,
null, null,

View File

@@ -48,25 +48,25 @@ pub const XMLHttpRequestEventTarget = struct {
typ: []const u8, typ: []const u8,
cbk: Callback, cbk: Callback,
) !void { ) !void {
const target = @as(*parser.EventTarget, @ptrCast(self));
const eh = try EventHandler.init(alloc, try cbk.withThis(target));
try parser.eventTargetAddEventListener( try parser.eventTargetAddEventListener(
@as(*parser.EventTarget, @ptrCast(self)), target,
alloc,
typ, typ,
EventHandler, &eh.node,
.{ .cbk = cbk },
false, false,
); );
} }
fn unregister(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void { fn unregister(self: *XMLHttpRequestEventTarget, typ: []const u8, cbk_id: usize) !void {
const et = @as(*parser.EventTarget, @ptrCast(self)); const et = @as(*parser.EventTarget, @ptrCast(self));
// check if event target has already this listener // check if event target has already this listener
const lst = try parser.eventTargetHasListener(et, typ, false, cbk.id); const lst = try parser.eventTargetHasListener(et, typ, false, cbk_id);
if (lst == null) { if (lst == null) {
return; return;
} }
// remove listener // remove listener
try parser.eventTargetRemoveEventListener(et, alloc, typ, lst.?, false); try parser.eventTargetRemoveEventListener(et, typ, lst.?, false);
} }
pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?Callback { pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?Callback {
@@ -89,39 +89,33 @@ pub const XMLHttpRequestEventTarget = struct {
} }
pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena; if (self.onloadstart_cbk) |cbk| try self.unregister("loadstart", cbk.id);
if (self.onloadstart_cbk) |cbk| try self.unregister(arena, "loadstart", cbk); try self.register(state.arena, "loadstart", handler);
try self.register(arena, "loadstart", handler);
self.onloadstart_cbk = handler; self.onloadstart_cbk = handler;
} }
pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena; if (self.onprogress_cbk) |cbk| try self.unregister("progress", cbk.id);
if (self.onprogress_cbk) |cbk| try self.unregister(arena, "progress", cbk); try self.register(state.arena, "progress", handler);
try self.register(arena, "progress", handler);
self.onprogress_cbk = handler; self.onprogress_cbk = handler;
} }
pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena; if (self.onabort_cbk) |cbk| try self.unregister("abort", cbk.id);
if (self.onabort_cbk) |cbk| try self.unregister(arena, "abort", cbk); try self.register(state.arena, "abort", handler);
try self.register(arena, "abort", handler);
self.onabort_cbk = handler; self.onabort_cbk = handler;
} }
pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena; if (self.onload_cbk) |cbk| try self.unregister("load", cbk.id);
if (self.onload_cbk) |cbk| try self.unregister(arena, "load", cbk); try self.register(state.arena, "load", handler);
try self.register(arena, "load", handler);
self.onload_cbk = handler; self.onload_cbk = handler;
} }
pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena; if (self.ontimeout_cbk) |cbk| try self.unregister("timeout", cbk.id);
if (self.ontimeout_cbk) |cbk| try self.unregister(arena, "timeout", cbk); try self.register(state.arena, "timeout", handler);
try self.register(arena, "timeout", handler);
self.ontimeout_cbk = handler; self.ontimeout_cbk = handler;
} }
pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void { pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Callback, state: *SessionState) !void {
const arena = state.arena; if (self.onloadend_cbk) |cbk| try self.unregister("loadend", cbk.id);
if (self.onloadend_cbk) |cbk| try self.unregister(arena, "loadend", cbk); try self.register(state.arena, "loadend", handler);
try self.register(arena, "loadend", handler);
self.onloadend_cbk = handler; self.onloadend_cbk = handler;
} }

View File

@@ -1200,7 +1200,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
pub const Callback = struct { pub const Callback = struct {
id: usize, id: usize,
executor: *Executor, executor: *Executor,
_this: ?v8.Object = null, this: ?v8.Object = null,
func: PersistentFunction, func: PersistentFunction,
// We use this when mapping a JS value to a Zig object. We can't // We use this when mapping a JS value to a Zig object. We can't
@@ -1215,8 +1215,13 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
exception: []const u8, exception: []const u8,
}; };
pub fn setThis(self: *Callback, value: anytype) !void { pub fn withThis(self: *const Callback, value: anytype) !Callback {
self._this = try self.executor.valueToExistingObject(value); return .{
.id = self.id,
.func = self.func,
.executor = self.executor,
.this = try self.executor.valueToExistingObject(value),
};
} }
pub fn call(self: *const Callback, args: anytype) !void { pub fn call(self: *const Callback, args: anytype) !void {
@@ -1264,7 +1269,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
} }
fn getThis(self: *const Callback) v8.Object { fn getThis(self: *const Callback) v8.Object {
return self._this orelse self.executor.context.getGlobal(); return self.this orelse self.executor.context.getGlobal();
} }
// debug/helper to print the source of the JS callback // debug/helper to print the source of the JS callback