Merge pull request #562 from lightpanda-io/mutation_observer
Some checks failed
e2e-test / zig build release (push) Has been cancelled
wpt / web platform tests (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled

Improve MutationObserver
This commit is contained in:
Pierre Tachoire
2025-04-25 10:53:48 +02:00
committed by GitHub
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 { fn init(alloc: std.mem.Allocator) MatchAll {
return .{ return .{
.alloc = alloc, .alloc = alloc,
.nl = NodeList.init(), .nl = .{},
}; };
} }
@@ -62,7 +62,8 @@ const MatchAll = struct {
} }
fn toOwnedList(m: *MatchAll) NodeList { fn toOwnedList(m: *MatchAll) NodeList {
defer m.nl = NodeList.init(); // reset it.
defer m.nl = .{};
return m.nl; return m.nl;
} }
}; };

View File

@@ -17,18 +17,17 @@
// 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 SessionState = @import("../env.zig").SessionState; const SessionState = @import("../env.zig").SessionState;
const Env = @import("../env.zig").Env; const Env = @import("../env.zig").Env;
const JsThis = @import("../env.zig").JsThis;
const NodeList = @import("nodelist.zig").NodeList; const NodeList = @import("nodelist.zig").NodeList;
pub const Interfaces = .{ pub const Interfaces = .{
MutationObserver, MutationObserver,
MutationRecord, MutationRecord,
MutationRecords,
}; };
const Walker = @import("../dom/walker.zig").WalkerChildren; 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 // WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
pub const MutationObserver = struct { pub const MutationObserver = struct {
cbk: Env.Callback, cbk: Env.Callback,
observers: Observers, arena: Allocator,
const Observer = struct { // List of records which were observed. When the scopeEnds, we need to
node: *parser.Node, // execute our callback with it.
options: MutationObserverInit, observed: std.ArrayListUnmanaged(*MutationRecord),
};
const Observers = std.ArrayListUnmanaged(*Observer); pub fn constructor(cbk: Env.Callback, state: *SessionState) !MutationObserver {
return .{
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{
.cbk = cbk, .cbk = cbk,
.observers = .{}, .observed = .{},
.arena = state.arena,
}; };
} }
// TODO pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?MutationObserverInit) !void {
fn resolveOptions(opt: ?MutationObserverInit) MutationObserverInit { const options = options_ orelse MutationObserverInit{};
return opt orelse .{};
}
pub fn _observe(self: *MutationObserver, node: *parser.Node, options: ?MutationObserverInit, state: *SessionState) !void { const observer = try self.arena.create(Observer);
const arena = state.arena; observer.* = .{
const o = try arena.create(Observer);
o.* = .{
.node = node, .node = node,
.options = resolveOptions(options), .options = options,
.mutation_observer = self,
}; };
errdefer arena.destroy(o);
// register the new observer. const arena = self.arena;
try self.observers.append(arena, o);
// register node's events. // register node's events
if (o.options.childList or o.options.subtree) { if (options.childList or options.subtree) {
try parser.eventTargetAddEventListener( try parser.eventTargetAddZigListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
arena, arena,
"DOMNodeInserted", "DOMNodeInserted",
EventHandler, Observer.handle,
.{ .cbk = self.cbk, .ctx = o }, observer,
false, false,
); );
try parser.eventTargetAddEventListener( try parser.eventTargetAddZigListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
arena, arena,
"DOMNodeRemoved", "DOMNodeRemoved",
EventHandler, Observer.handle,
.{ .cbk = self.cbk, .ctx = o }, observer,
false, false,
); );
} }
if (o.options.attr()) { if (options.attr()) {
try parser.eventTargetAddEventListener( try parser.eventTargetAddZigListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
arena, arena,
"DOMAttrModified", "DOMAttrModified",
EventHandler, Observer.handle,
.{ .cbk = self.cbk, .ctx = o }, observer,
false, false,
); );
} }
if (o.options.cdata()) { if (options.cdata()) {
try parser.eventTargetAddEventListener( try parser.eventTargetAddZigListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
arena, arena,
"DOMCharacterDataModified", "DOMCharacterDataModified",
EventHandler, Observer.handle,
.{ .cbk = self.cbk, .ctx = o }, observer,
false, false,
); );
} }
if (o.options.subtree) { if (options.subtree) {
try parser.eventTargetAddEventListener( try parser.eventTargetAddZigListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
arena, arena,
"DOMSubtreeModified", "DOMSubtreeModified",
EventHandler, Observer.handle,
.{ .cbk = self.cbk, .ctx = o }, observer,
false, 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 // TODO
pub fn _disconnect(_: *MutationObserver) !void { pub fn _disconnect(_: *MutationObserver) !void {
// TODO unregister listeners. // 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 { pub const MutationRecord = struct {
type: []const u8, type: []const u8,
target: *parser.Node, target: *parser.Node,
addedNodes: NodeList = NodeList.init(), added_nodes: NodeList = .{},
removedNodes: NodeList = NodeList.init(), removed_nodes: NodeList = .{},
previousSibling: ?*parser.Node = null, previous_sibling: ?*parser.Node = null,
nextSibling: ?*parser.Node = null, next_sibling: ?*parser.Node = null,
attributeName: ?[]const u8 = null, attribute_name: ?[]const u8 = null,
attributeNamespace: ?[]const u8 = null, attribute_namespace: ?[]const u8 = null,
oldValue: ?[]const u8 = null, old_value: ?[]const u8 = null,
pub fn get_type(self: *const MutationRecord) []const u8 { pub fn get_type(self: *const MutationRecord) []const u8 {
return self.type; return self.type;
} }
pub fn get_addedNodes(self: *const MutationRecord) NodeList { pub fn get_addedNodes(self: *MutationRecord) *NodeList {
return self.addedNodes; return &self.added_nodes;
} }
pub fn get_removedNodes(self: *const MutationRecord) NodeList { pub fn get_removedNodes(self: *MutationRecord) *NodeList {
return self.addedNodes; return &self.removed_nodes;
} }
pub fn get_target(self: *const MutationRecord) *parser.Node { 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 { 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 { 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 { 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 { 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 { pub fn get_oldValue(self: *const MutationRecord) ?[]const u8 {
return self.oldValue; return self.old_value;
} }
}; };
// EventHandler dedicated to mutation events. const MutationObserverInit = struct {
const EventHandler = struct { childList: bool = false,
fn apply(o: *MutationObserver.Observer, target: *parser.Node) bool { 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 records.
mutation_observer: *MutationObserver,
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) return true; if (o.options.subtree) {
return true;
}
// if target equals node, alway ok. // 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. // 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 // target must be a child of o.node
const walker = Walker{}; const walker = Walker{};
var next: ?*parser.Node = null; var next: ?*parser.Node = null;
while (true) { while (true) {
next = walker.get_next(o.node, next) catch break orelse break; next = walker.get_next(o.node, next) catch break orelse break;
if (next.? == target) return true; if (next.? == target) {
return true;
}
} }
return false; return false;
} }
fn handle(evt: ?*parser.Event, data: *const parser.JSEventHandlerData) void { fn handle(ctx: *anyopaque, event: *parser.Event) 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);
// retrieve the observer from the data. // 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? if (self.appliesTo(node) == false) {
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 {
return; return;
} }
// TODO pass MutationRecords and MutationObserver const event_type = blk: {
var result: Env.Callback.Result = undefined; const t = parser.eventType(event) catch |e| {
data.cbk.tryCall(.{mrs}, &result) catch { log.err("mutation observer event type: {any}", .{e});
log.err("mutation observer callback error: {s}", .{result.exception}); return;
log.debug("stack:\n{s}", .{result.stack orelse "???"}); };
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"); const testing = @import("../../testing.zig");
test "Browser.DOM.MutationObserver" { test "Browser.DOM.MutationObserver" {
@@ -384,4 +388,19 @@ test "Browser.DOM.MutationObserver" {
.{ "mrs2[0].target.data", "foo" }, .{ "mrs2[0].target.data", "foo" },
.{ "mrs2[0].oldValue", " And" }, .{ "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 { pub fn get_childNodes(self: *parser.Node, state: *SessionState) !NodeList {
const allocator = state.arena; const allocator = state.arena;
var list = NodeList.init(); var list: NodeList = .{};
errdefer list.deinit(allocator);
var n = try parser.nodeFirstChild(self) orelse return list; var n = try parser.nodeFirstChild(self) orelse return list;
while (true) { while (true) {

View File

@@ -99,16 +99,9 @@ pub const NodeListEntriesIterator = struct {
// see https://dom.spec.whatwg.org/#old-style-collections // see https://dom.spec.whatwg.org/#old-style-collections
pub const NodeList = struct { pub const NodeList = struct {
pub const Exception = DOMException; pub const Exception = DOMException;
const NodesArrayList = std.ArrayListUnmanaged(*parser.Node); const NodesArrayList = std.ArrayListUnmanaged(*parser.Node);
nodes: NodesArrayList, nodes: NodesArrayList = .{},
pub fn init() NodeList {
return NodeList{
.nodes = NodesArrayList{},
};
}
pub fn deinit(self: *NodeList, alloc: std.mem.Allocator) void { pub fn deinit(self: *NodeList, alloc: std.mem.Allocator) void {
// TODO unref all nodes // 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 { pub fn _getElementsByName(self: *parser.DocumentHTML, name: []const u8, state: *SessionState) !NodeList {
const arena = state.arena; const arena = state.arena;
var list = NodeList.init(); var list: NodeList = .{};
errdefer list.deinit(arena);
if (name.len == 0) return list; 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

@@ -664,11 +664,22 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
const T = @TypeOf(value); const T = @TypeOf(value);
switch (@typeInfo(T)) { 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 // Need to do this to keep the compiler happy
// simpleZigValueToJs handles all of these cases. // simpleZigValueToJs handles all of these cases.
unreachable; 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) { .pointer => |ptr| switch (ptr.size) {
.one => { .one => {
const type_name = @typeName(ptr.child); const type_name = @typeName(ptr.child);
@@ -1021,6 +1032,10 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
return gop.value_ptr.*; 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 // Sometimes we're creating a new v8.Object, like when
// we're returning a value from a function. In those cases // we're returning a value from a function. In those cases
// we have the FunctionTemplate, and we can get an object // we have the FunctionTemplate, and we can get an object
@@ -1134,6 +1149,7 @@ pub fn Env(comptime S: type, comptime types: anytype) type {
pub const Scope = struct { pub const Scope = struct {
arena: Allocator, arena: Allocator,
handle_scope: v8.HandleScope, handle_scope: v8.HandleScope,
scope_end_callbacks: std.ArrayListUnmanaged(ScopeEndCallback) = .{},
callbacks: std.ArrayListUnmanaged(v8.Persistent(v8.Function)) = .{}, callbacks: std.ArrayListUnmanaged(v8.Persistent(v8.Function)) = .{},
identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .{}, identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .{},
@@ -1153,6 +1169,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 { pub const Callback = struct {
id: usize, id: usize,
executor: *Executor, executor: *Executor,
@@ -1575,7 +1619,6 @@ fn Caller(comptime E: type) type {
fn deinit(self: *Self) void { fn deinit(self: *Self) void {
const executor = self.executor; const executor = self.executor;
const call_depth = executor.call_depth - 1; const call_depth = executor.call_depth - 1;
executor.call_depth = call_depth;
// Because of callbacks, calls can be nested. Because of this, we // Because of callbacks, calls can be nested. Because of this, we
// can't clear the call_arena after _every_ call. Imagine we have // can't clear the call_arena after _every_ call. Imagine we have
@@ -1588,9 +1631,20 @@ fn Caller(comptime E: type) type {
// //
// Therefore, we keep a call_depth, and only reset the call_arena // Therefore, we keep a call_depth, and only reset the call_arena
// when a top-level (call_depth == 0) function ends. // when a top-level (call_depth == 0) function ends.
if (call_depth == 0) { 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 }); _ = 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 { fn constructor(self: *Self, comptime named_function: anytype, info: v8.FunctionCallbackInfo) !void {