Improve MutationObserver

- Fix get_removedNodes (it was returning addedNodes)
- get_removedNodes and get addedNodes now return references
- used enum for dispatching and clean up dispatching in general
- Remove MutationRecords and simply return an array
  - this allows the returned records to be iterable (as they should be)
  - jsruntime ZigToJs will now map a Zig array to a JS array
-Rely on default initialize of NodeList
-Batch observed records
 - Callback only executed when call_depth == 0
 - Fixes crashes when a MutationObserver callback mutated the nodes being
   observes.
 - Fixes some WPT issues, but Netsurf's mutationEventRelatedNode does not
   appear to be to spec, so most tests fail.
 - Allow zig methods to execute arbitrary code when call_depth == 0
   - This is a preview of how I hope to make XHR not crash if the CDP session
     ends while there's still network activity
This commit is contained in:
Karl Seguin
2025-04-21 08:53:46 +08:00
parent 332508f563
commit 4c89bb0e0a
7 changed files with 269 additions and 467 deletions

View File

@@ -49,7 +49,7 @@ const MatchAll = struct {
fn init(alloc: std.mem.Allocator) MatchAll {
return .{
.alloc = alloc,
.nl = NodeList.init(),
.nl = .{},
};
}
@@ -62,7 +62,8 @@ const MatchAll = struct {
}
fn toOwnedList(m: *MatchAll) NodeList {
defer m.nl = NodeList.init();
// reset it.
defer m.nl = .{};
return m.nl;
}
};

View File

@@ -17,18 +17,17 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const parser = @import("../netsurf.zig");
const SessionState = @import("../env.zig").SessionState;
const Env = @import("../env.zig").Env;
const JsThis = @import("../env.zig").JsThis;
const NodeList = @import("nodelist.zig").NodeList;
pub const Interfaces = .{
MutationObserver,
MutationRecord,
MutationRecords,
};
const Walker = @import("../dom/walker.zig").WalkerChildren;
@@ -38,109 +37,101 @@ const log = std.log.scoped(.events);
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
pub const MutationObserver = struct {
cbk: Env.Callback,
observers: Observers,
arena: Allocator,
const Observer = struct {
node: *parser.Node,
options: MutationObserverInit,
};
// List of records which were observed. When the scopeEnds, we need to
// execute our callback with it.
observed: std.ArrayListUnmanaged(*MutationRecord),
const Observers = std.ArrayListUnmanaged(*Observer);
pub const MutationObserverInit = struct {
childList: bool = false,
attributes: bool = false,
characterData: bool = false,
subtree: bool = false,
attributeOldValue: bool = false,
characterDataOldValue: bool = false,
// TODO
// attributeFilter: [][]const u8,
fn attr(self: MutationObserverInit) bool {
return self.attributes or self.attributeOldValue;
}
fn cdata(self: MutationObserverInit) bool {
return self.characterData or self.characterDataOldValue;
}
};
pub fn constructor(cbk: Env.Callback) !MutationObserver {
return MutationObserver{
pub fn constructor(cbk: Env.Callback, state: *SessionState) !MutationObserver {
return .{
.cbk = cbk,
.observers = .{},
.observed = .{},
.arena = state.arena,
};
}
// TODO
fn resolveOptions(opt: ?MutationObserverInit) MutationObserverInit {
return opt orelse .{};
}
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?MutationObserverInit) !void {
const options = options_ orelse MutationObserverInit{};
pub fn _observe(self: *MutationObserver, node: *parser.Node, options: ?MutationObserverInit, state: *SessionState) !void {
const arena = state.arena;
const o = try arena.create(Observer);
o.* = .{
const observer = try self.arena.create(Observer);
observer.* = .{
.node = node,
.options = resolveOptions(options),
.options = options,
.mutation_observer = self,
};
errdefer arena.destroy(o);
// register the new observer.
try self.observers.append(arena, o);
const arena = self.arena;
// register node's events.
if (o.options.childList or o.options.subtree) {
try parser.eventTargetAddEventListener(
// register node's events
if (options.childList or options.subtree) {
try parser.eventTargetAddZigListener(
parser.toEventTarget(parser.Node, node),
arena,
"DOMNodeInserted",
EventHandler,
.{ .cbk = self.cbk, .ctx = o },
Observer.handle,
observer,
false,
);
try parser.eventTargetAddEventListener(
try parser.eventTargetAddZigListener(
parser.toEventTarget(parser.Node, node),
arena,
"DOMNodeRemoved",
EventHandler,
.{ .cbk = self.cbk, .ctx = o },
Observer.handle,
observer,
false,
);
}
if (o.options.attr()) {
try parser.eventTargetAddEventListener(
if (options.attr()) {
try parser.eventTargetAddZigListener(
parser.toEventTarget(parser.Node, node),
arena,
"DOMAttrModified",
EventHandler,
.{ .cbk = self.cbk, .ctx = o },
Observer.handle,
observer,
false,
);
}
if (o.options.cdata()) {
try parser.eventTargetAddEventListener(
if (options.cdata()) {
try parser.eventTargetAddZigListener(
parser.toEventTarget(parser.Node, node),
arena,
"DOMCharacterDataModified",
EventHandler,
.{ .cbk = self.cbk, .ctx = o },
Observer.handle,
observer,
false,
);
}
if (o.options.subtree) {
try parser.eventTargetAddEventListener(
if (options.subtree) {
try parser.eventTargetAddZigListener(
parser.toEventTarget(parser.Node, node),
arena,
"DOMSubtreeModified",
EventHandler,
.{ .cbk = self.cbk, .ctx = o },
Observer.handle,
observer,
false,
);
}
}
pub fn jsScopeEnd(self: *MutationObserver, _: anytype) void {
const record = self.observed.items;
if (record.len == 0) {
return;
}
defer self.observed.clearRetainingCapacity();
for (record) |r| {
const records = [_]MutationRecord{r.*};
var result: Env.Callback.Result = undefined;
self.cbk.tryCall(.{records}, &result) catch {
log.err("mutation observer callback error: {s}", .{result.exception});
log.debug("stack:\n{s}", .{result.stack orelse "???"});
};
}
}
// TODO
pub fn _disconnect(_: *MutationObserver) !void {
// TODO unregister listeners.
@@ -152,50 +143,27 @@ pub const MutationObserver = struct {
}
};
// Handle multiple record?
pub const MutationRecords = struct {
first: ?MutationRecord = null,
pub fn get_length(self: *const MutationRecords) u32 {
if (self.first == null) return 0;
return 1;
}
pub fn indexed_get(self: *const MutationRecords, i: u32, has_value: *bool) ?MutationRecord {
_ = i;
return self.first orelse {
has_value.* = false;
return null;
};
}
pub fn postAttach(self: *const MutationRecords, js_this: JsThis) !void {
if (self.first) |mr| {
try js_this.set("0", mr);
}
}
};
pub const MutationRecord = struct {
type: []const u8,
target: *parser.Node,
addedNodes: NodeList = NodeList.init(),
removedNodes: NodeList = NodeList.init(),
previousSibling: ?*parser.Node = null,
nextSibling: ?*parser.Node = null,
attributeName: ?[]const u8 = null,
attributeNamespace: ?[]const u8 = null,
oldValue: ?[]const u8 = null,
added_nodes: NodeList = .{},
removed_nodes: NodeList = .{},
previous_sibling: ?*parser.Node = null,
next_sibling: ?*parser.Node = null,
attribute_name: ?[]const u8 = null,
attribute_namespace: ?[]const u8 = null,
old_value: ?[]const u8 = null,
pub fn get_type(self: *const MutationRecord) []const u8 {
return self.type;
}
pub fn get_addedNodes(self: *const MutationRecord) NodeList {
return self.addedNodes;
pub fn get_addedNodes(self: *MutationRecord) *NodeList {
return &self.added_nodes;
}
pub fn get_removedNodes(self: *const MutationRecord) NodeList {
return self.addedNodes;
pub fn get_removedNodes(self: *MutationRecord) *NodeList {
return &self.removed_nodes;
}
pub fn get_target(self: *const MutationRecord) *parser.Node {
@@ -203,136 +171,172 @@ pub const MutationRecord = struct {
}
pub fn get_attributeName(self: *const MutationRecord) ?[]const u8 {
return self.attributeName;
return self.attribute_name;
}
pub fn get_attributeNamespace(self: *const MutationRecord) ?[]const u8 {
return self.attributeNamespace;
return self.attribute_namespace;
}
pub fn get_previousSibling(self: *const MutationRecord) ?*parser.Node {
return self.previousSibling;
return self.previous_sibling;
}
pub fn get_nextSibling(self: *const MutationRecord) ?*parser.Node {
return self.nextSibling;
return self.next_sibling;
}
pub fn get_oldValue(self: *const MutationRecord) ?[]const u8 {
return self.oldValue;
return self.old_value;
}
};
// EventHandler dedicated to mutation events.
const EventHandler = struct {
fn apply(o: *MutationObserver.Observer, target: *parser.Node) bool {
const MutationObserverInit = struct {
childList: bool = false,
attributes: bool = false,
characterData: bool = false,
subtree: bool = false,
attributeOldValue: bool = false,
characterDataOldValue: bool = false,
// TODO
// attributeFilter: [][]const u8,
fn attr(self: MutationObserverInit) bool {
return self.attributes or self.attributeOldValue;
}
fn cdata(self: MutationObserverInit) bool {
return self.characterData or self.characterDataOldValue;
}
};
const Observer = struct {
node: *parser.Node,
options: MutationObserverInit,
// record of the mutation, all observed changes in 1 call are batched
record: ?MutationRecord = null,
// reference back to the MutationObserver so that we can access the arena
// and batch the mutation record.s
mutation_observer: *MutationObserver,
fn appliesTo(o: *const Observer, target: *parser.Node) bool {
// mutation on any target is always ok.
if (o.options.subtree) return true;
if (o.options.subtree) {
return true;
}
// if target equals node, alway ok.
if (target == o.node) return true;
if (target == o.node) {
return true;
}
// no subtree, no same target and no childlist, always noky.
if (!o.options.childList) return false;
if (!o.options.childList) {
return false;
}
// target must be a child of o.node
const walker = Walker{};
var next: ?*parser.Node = null;
while (true) {
next = walker.get_next(o.node, next) catch break orelse break;
if (next.? == target) return true;
if (next.? == target) {
return true;
}
}
return false;
}
fn handle(evt: ?*parser.Event, data: *const parser.JSEventHandlerData) void {
if (evt == null) return;
var mrs: MutationRecords = .{};
const t = parser.eventType(evt.?) catch |e| {
log.err("mutation observer event type: {any}", .{e});
return;
};
const et = parser.eventTarget(evt.?) catch |e| {
log.err("mutation observer event target: {any}", .{e});
return;
} orelse return;
const node = parser.eventTargetToNode(et);
fn handle(ctx: *anyopaque, event: *parser.Event) void {
// retrieve the observer from the data.
const o: *MutationObserver.Observer = @ptrCast(@alignCast(data.ctx));
var self: *Observer = @alignCast(@ptrCast(ctx));
var mutation_observer = self.mutation_observer;
if (!apply(o, node)) return;
const node = blk: {
const event_target = parser.eventTarget(event) catch |e| {
log.err("mutation observer event target: {any}", .{e});
return;
} orelse return;
const muevt = parser.eventToMutationEvent(evt.?);
break :blk parser.eventTargetToNode(event_target);
};
// TODO get the allocator by another way?
const alloc = data.cbk.executor.scope_arena;
if (std.mem.eql(u8, t, "DOMAttrModified")) {
mrs.first = .{
.type = "attributes",
.target = o.node,
.attributeName = parser.mutationEventAttributeName(muevt) catch null,
};
// record old value if required.
if (o.options.attributeOldValue) {
mrs.first.?.oldValue = parser.mutationEventPrevValue(muevt) catch null;
}
} else if (std.mem.eql(u8, t, "DOMCharacterDataModified")) {
mrs.first = .{
.type = "characterData",
.target = o.node,
};
// record old value if required.
if (o.options.characterDataOldValue) {
mrs.first.?.oldValue = parser.mutationEventPrevValue(muevt) catch null;
}
} else if (std.mem.eql(u8, t, "DOMNodeInserted")) {
mrs.first = .{
.type = "childList",
.target = o.node,
.addedNodes = NodeList.init(),
.removedNodes = NodeList.init(),
};
const rn = parser.mutationEventRelatedNode(muevt) catch null;
if (rn) |n| {
mrs.first.?.addedNodes.append(alloc, n) catch |e| {
log.err("mutation event handler error: {any}", .{e});
return;
};
}
} else if (std.mem.eql(u8, t, "DOMNodeRemoved")) {
mrs.first = .{
.type = "childList",
.target = o.node,
.addedNodes = NodeList.init(),
.removedNodes = NodeList.init(),
};
const rn = parser.mutationEventRelatedNode(muevt) catch null;
if (rn) |n| {
mrs.first.?.removedNodes.append(alloc, n) catch |e| {
log.err("mutation event handler error: {any}", .{e});
return;
};
}
} else {
if (self.appliesTo(node) == false) {
return;
}
// TODO pass MutationRecords and MutationObserver
var result: Env.Callback.Result = undefined;
data.cbk.tryCall(.{mrs}, &result) catch {
log.err("mutation observer callback error: {s}", .{result.exception});
log.debug("stack:\n{s}", .{result.stack orelse "???"});
const event_type = blk: {
const t = parser.eventType(event) catch |e| {
log.err("mutation observer event type: {any}", .{e});
return;
};
break :blk std.meta.stringToEnum(MutationEventType, t) orelse return;
};
const arena = mutation_observer.arena;
if (self.record == null) {
self.record = .{
.target = self.node,
.type = event_type.recordType(),
};
mutation_observer.observed.append(arena, &self.record.?) catch |err| {
log.err("mutation_observer append: {}", .{err});
};
}
var record = &self.record.?;
const mutation_event = parser.eventToMutationEvent(event);
switch (event_type) {
.DOMAttrModified => {
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
if (self.options.attributeOldValue) {
record.old_value = parser.mutationEventPrevValue(mutation_event) catch null;
}
},
.DOMCharacterDataModified => {
if (self.options.characterDataOldValue) {
record.old_value = parser.mutationEventPrevValue(mutation_event) catch null;
}
},
.DOMNodeInserted => {
if (parser.mutationEventRelatedNode(mutation_event) catch null) |related_node| {
record.added_nodes.append(arena, related_node) catch |e| {
log.err("mutation event handler error: {any}", .{e});
return;
};
}
},
.DOMNodeRemoved => {
if (parser.mutationEventRelatedNode(mutation_event) catch null) |related_node| {
record.removed_nodes.append(arena, related_node) catch |e| {
log.err("mutation event handler error: {any}", .{e});
return;
};
}
},
}
}
};
const MutationEventType = enum {
DOMAttrModified,
DOMCharacterDataModified,
DOMNodeInserted,
DOMNodeRemoved,
fn recordType(self: MutationEventType) []const u8 {
return switch (self) {
.DOMAttrModified => "attributes",
.DOMCharacterDataModified => "characterData",
.DOMNodeInserted => "childList",
.DOMNodeRemoved => "childList",
};
}
}.handle;
};
const testing = @import("../../testing.zig");
test "Browser.DOM.MutationObserver" {
@@ -384,4 +388,19 @@ test "Browser.DOM.MutationObserver" {
.{ "mrs2[0].target.data", "foo" },
.{ "mrs2[0].oldValue", " And" },
}, .{});
// tests that mutation observers that have a callback which trigger the
// mutation observer don't crash.
// https://github.com/lightpanda-io/browser/issues/550
try runner.testCases(&.{
.{
\\ var node = document.getElementById("para");
\\ new MutationObserver(() => {
\\ node.innerText = 'a';
\\ }).observe(document, { subtree:true,childList:true });
\\ node.innerText = "2";
,
"2",
},
}, .{});
}

View File

@@ -275,8 +275,7 @@ pub const Node = struct {
pub fn get_childNodes(self: *parser.Node, state: *SessionState) !NodeList {
const allocator = state.arena;
var list = NodeList.init();
errdefer list.deinit(allocator);
var list: NodeList = .{};
var n = try parser.nodeFirstChild(self) orelse return list;
while (true) {

View File

@@ -99,16 +99,9 @@ pub const NodeListEntriesIterator = struct {
// see https://dom.spec.whatwg.org/#old-style-collections
pub const NodeList = struct {
pub const Exception = DOMException;
const NodesArrayList = std.ArrayListUnmanaged(*parser.Node);
nodes: NodesArrayList,
pub fn init() NodeList {
return NodeList{
.nodes = NodesArrayList{},
};
}
nodes: NodesArrayList = .{},
pub fn deinit(self: *NodeList, alloc: std.mem.Allocator) void {
// TODO unref all nodes

View File

@@ -103,8 +103,7 @@ pub const HTMLDocument = struct {
pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, state: *SessionState) !NodeList {
const arena = state.arena;
var list = NodeList.init();
errdefer list.deinit(arena);
var list: NodeList = .{};
if (name.len == 0) return list;

View File

@@ -1,263 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const generate = @import("../generate.zig");
const jsruntime = @import("jsruntime");
const Callback = jsruntime.Callback;
const CallbackResult = jsruntime.CallbackResult;
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const parser = @import("netsurf");
const DOMException = @import("../dom/exceptions.zig").DOMException;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventTargetUnion = @import("../dom/event_target.zig").Union;
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
const log = std.log.scoped(.events);
// Event interfaces
pub const Interfaces = .{
Event,
ProgressEvent,
};
pub const Union = generate.Union(Interfaces);
// https://dom.spec.whatwg.org/#event
pub const Event = struct {
pub const Self = parser.Event;
pub const Exception = DOMException;
pub const mem_guarantied = true;
pub const EventInit = parser.EventInit;
// JS
// --
pub const _CAPTURING_PHASE = 1;
pub const _AT_TARGET = 2;
pub const _BUBBLING_PHASE = 3;
pub fn toInterface(evt: *parser.Event) !Union {
return switch (try parser.eventGetInternalType(evt)) {
.event => .{ .Event = evt },
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
};
}
pub fn constructor(eventType: []const u8, opts: ?EventInit) !*parser.Event {
const event = try parser.eventCreate();
try parser.eventInit(event, eventType, opts orelse EventInit{});
return event;
}
// Getters
pub fn get_type(self: *parser.Event) ![]const u8 {
return try parser.eventType(self);
}
pub fn get_target(self: *parser.Event) !?EventTargetUnion {
const et = try parser.eventTarget(self);
if (et == null) return null;
return try EventTarget.toInterface(et.?);
}
pub fn get_currentTarget(self: *parser.Event) !?EventTargetUnion {
const et = try parser.eventCurrentTarget(self);
if (et == null) return null;
return try EventTarget.toInterface(et.?);
}
pub fn get_eventPhase(self: *parser.Event) !u8 {
return try parser.eventPhase(self);
}
pub fn get_bubbles(self: *parser.Event) !bool {
return try parser.eventBubbles(self);
}
pub fn get_cancelable(self: *parser.Event) !bool {
return try parser.eventCancelable(self);
}
pub fn get_defaultPrevented(self: *parser.Event) !bool {
return try parser.eventDefaultPrevented(self);
}
pub fn get_isTrusted(self: *parser.Event) !bool {
return try parser.eventIsTrusted(self);
}
pub fn get_timestamp(self: *parser.Event) !u32 {
return try parser.eventTimestamp(self);
}
// Methods
pub fn _initEvent(
self: *parser.Event,
eventType: []const u8,
bubbles: ?bool,
cancelable: ?bool,
) !void {
const opts = EventInit{
.bubbles = bubbles orelse false,
.cancelable = cancelable orelse false,
};
return try parser.eventInit(self, eventType, opts);
}
pub fn _stopPropagation(self: *parser.Event) !void {
return try parser.eventStopPropagation(self);
}
pub fn _stopImmediatePropagation(self: *parser.Event) !void {
return try parser.eventStopImmediatePropagation(self);
}
pub fn _preventDefault(self: *parser.Event) !void {
return try parser.eventPreventDefault(self);
}
};
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var common = [_]Case{
.{ .src = "let content = document.getElementById('content')", .ex = "undefined" },
.{ .src = "let para = document.getElementById('para')", .ex = "undefined" },
.{ .src = "var nb = 0; var evt", .ex = "undefined" },
};
try checkCases(js_env, &common);
var basic = [_]Case{
.{ .src =
\\content.addEventListener('target',
\\function(e) {
\\evt = e; nb = nb + 1;
\\e.preventDefault();
\\})
, .ex = "undefined" },
.{ .src = "content.dispatchEvent(new Event('target', {bubbles: true, cancelable: true}))", .ex = "false" },
.{ .src = "nb", .ex = "1" },
.{ .src = "evt.target === content", .ex = "true" },
.{ .src = "evt.bubbles", .ex = "true" },
.{ .src = "evt.cancelable", .ex = "true" },
.{ .src = "evt.defaultPrevented", .ex = "true" },
.{ .src = "evt.isTrusted", .ex = "true" },
.{ .src = "evt.timestamp > 1704063600", .ex = "true" }, // 2024/01/01 00:00
// event.type, event.currentTarget, event.phase checked in EventTarget
};
try checkCases(js_env, &basic);
var stop = [_]Case{
.{ .src = "nb = 0", .ex = "0" },
.{ .src =
\\content.addEventListener('stop',
\\function(e) {
\\e.stopPropagation();
\\nb = nb + 1;
\\}, true)
, .ex = "undefined" },
// the following event listener will not be invoked
.{ .src =
\\para.addEventListener('stop',
\\function(e) {
\\nb = nb + 1;
\\})
, .ex = "undefined" },
.{ .src = "para.dispatchEvent(new Event('stop'))", .ex = "true" },
.{ .src = "nb", .ex = "1" }, // will be 2 if event was not stopped at content event listener
};
try checkCases(js_env, &stop);
var stop_immediate = [_]Case{
.{ .src = "nb = 0", .ex = "0" },
.{ .src =
\\content.addEventListener('immediate',
\\function(e) {
\\e.stopImmediatePropagation();
\\nb = nb + 1;
\\})
, .ex = "undefined" },
// the following event listener will not be invoked
.{ .src =
\\content.addEventListener('immediate',
\\function(e) {
\\nb = nb + 1;
\\})
, .ex = "undefined" },
.{ .src = "content.dispatchEvent(new Event('immediate'))", .ex = "true" },
.{ .src = "nb", .ex = "1" }, // will be 2 if event was not stopped at first content event listener
};
try checkCases(js_env, &stop_immediate);
var legacy = [_]Case{
.{ .src = "nb = 0", .ex = "0" },
.{ .src =
\\content.addEventListener('legacy',
\\function(e) {
\\evt = e; nb = nb + 1;
\\})
, .ex = "undefined" },
.{ .src = "let evtLegacy = document.createEvent('Event')", .ex = "undefined" },
.{ .src = "evtLegacy.initEvent('legacy')", .ex = "undefined" },
.{ .src = "content.dispatchEvent(evtLegacy)", .ex = "true" },
.{ .src = "nb", .ex = "1" },
};
try checkCases(js_env, &legacy);
var remove = [_]Case{
.{ .src = "var nb = 0; var evt = null; function cbk(event) { nb ++; evt=event; }", .ex = "undefined" },
.{ .src = "document.addEventListener('count', cbk)", .ex = "undefined" },
.{ .src = "document.removeEventListener('count', cbk)", .ex = "undefined" },
.{ .src = "document.dispatchEvent(new Event('count'))", .ex = "true" },
.{ .src = "nb", .ex = "0" },
};
try checkCases(js_env, &remove);
}
pub const EventHandler = struct {
fn handle(event: ?*parser.Event, data: *const parser.JSEventHandlerData) void {
// TODO get the allocator by another way?
var res = CallbackResult.init(data.cbk.nat_ctx.alloc);
defer res.deinit();
if (event) |evt| {
data.cbk.trycall(.{
Event.toInterface(evt) catch unreachable,
}, &res) catch |e| log.err("event handler error: {any}", .{e});
} else {
data.cbk.trycall(.{event}, &res) catch |e| log.err("event handler error (null event): {any}", .{e});
}
// in case of function error, we log the result and the trace.
if (!res.success) {
log.info("event handler error try catch: {s}", .{res.result orelse "unknown"});
log.debug("{s}", .{res.stack orelse "no stack trace"});
}
}
}.handle;

View File

@@ -661,11 +661,22 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
const T = @TypeOf(value);
switch (@typeInfo(T)) {
.void, .bool, .int, .comptime_int, .float, .comptime_float, .array => {
.void, .bool, .int, .comptime_int, .float, .comptime_float => {
// Need to do this to keep the compiler happy
// simpleZigValueToJs handles all of these cases.
unreachable;
},
.array => {
var js_arr = v8.Array.init(isolate, value.len);
var js_obj = js_arr.castTo(v8.Object);
for (value, 0..) |v, i| {
const js_val = try zigValueToJs(templates, isolate, context, v);
if (js_obj.setValueAtIndex(context, @intCast(i), js_val) == false) {
return error.FailedToCreateArray;
}
}
return js_obj.toValue();
},
.pointer => |ptr| switch (ptr.size) {
.one => {
const type_name = @typeName(ptr.child);
@@ -1018,6 +1029,10 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
return gop.value_ptr.*;
}
if (comptime @hasDecl(ptr.child, "jsScopeEnd")) {
try scope.scope_end_callbacks.append(scope_arena, ScopeEndCallback.init(value));
}
// Sometimes we're creating a new v8.Object, like when
// we're returning a value from a function. In those cases
// we have the FunctionTemplate, and we can get an object
@@ -1131,6 +1146,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
pub const Scope = struct {
arena: Allocator,
handle_scope: v8.HandleScope,
scope_end_callbacks: std.ArrayListUnmanaged(ScopeEndCallback) = .{},
callbacks: std.ArrayListUnmanaged(v8.Persistent(v8.Function)) = .{},
identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .{},
@@ -1150,6 +1166,34 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
}
};
// An interface for types that want to their jsScopeEnd function to be
// called when the scope ends
const ScopeEndCallback = struct {
ptr: *anyopaque,
scopeEndFn: *const fn (ptr: *anyopaque, executor: *Executor) void,
fn init(ptr: anytype) ScopeEndCallback {
const T = @TypeOf(ptr);
const ptr_info = @typeInfo(T);
const gen = struct {
pub fn scopeEnd(pointer: *anyopaque, executor: *Executor) void {
const self: T = @ptrCast(@alignCast(pointer));
return ptr_info.pointer.child.jsScopeEnd(self, executor);
}
};
return .{
.ptr = ptr,
.scopeEndFn = gen.scopeEnd,
};
}
pub fn scopeEnd(self: ScopeEndCallback, executor: *Executor) void {
self.scopeEndFn(self.ptr, executor);
}
};
pub const Callback = struct {
id: usize,
executor: *Executor,
@@ -1572,7 +1616,6 @@ fn Caller(comptime E: type) type {
fn deinit(self: *Self) void {
const executor = self.executor;
const call_depth = executor.call_depth - 1;
executor.call_depth = call_depth;
// Because of callbacks, calls can be nested. Because of this, we
// can't clear the call_arena after _every_ call. Imagine we have
@@ -1585,9 +1628,20 @@ fn Caller(comptime E: type) type {
//
// Therefore, we keep a call_depth, and only reset the call_arena
// when a top-level (call_depth == 0) function ends.
if (call_depth == 0) {
const scope = &self.executor.scope.?;
for (scope.scope_end_callbacks.items) |cb| {
cb.scopeEnd(executor);
}
_ = executor._call_arena_instance.reset(.{ .retain_with_limit = CALL_ARENA_RETAIN });
}
// Set this _after_ we've executed the above code, so that if the
// above code executes any callbacks, they aren't being executed
// at scope 0, which would be wrong.
executor.call_depth = call_depth;
}
fn constructor(self: *Self, comptime named_function: anytype, info: v8.FunctionCallbackInfo) !void {