diff --git a/src/browser/dom/css.zig b/src/browser/dom/css.zig index 50c262e4..c9f3d647 100644 --- a/src/browser/dom/css.zig +++ b/src/browser/dom/css.zig @@ -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; } }; diff --git a/src/browser/dom/mutation_observer.zig b/src/browser/dom/mutation_observer.zig index c6b57b8d..8aac8f3b 100644 --- a/src/browser/dom/mutation_observer.zig +++ b/src/browser/dom/mutation_observer.zig @@ -17,18 +17,17 @@ // along with this program. If not, see . 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 records. + 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", + }, + }, .{}); } diff --git a/src/browser/dom/node.zig b/src/browser/dom/node.zig index 1e1aba1b..22f41dce 100644 --- a/src/browser/dom/node.zig +++ b/src/browser/dom/node.zig @@ -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) { diff --git a/src/browser/dom/nodelist.zig b/src/browser/dom/nodelist.zig index 37ef3b31..edb0d1f0 100644 --- a/src/browser/dom/nodelist.zig +++ b/src/browser/dom/nodelist.zig @@ -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 diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig index f708dc1c..b7437c5e 100644 --- a/src/browser/html/document.zig +++ b/src/browser/html/document.zig @@ -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; diff --git a/src/events/event.zig b/src/events/event.zig deleted file mode 100644 index e37d0512..00000000 --- a/src/events/event.zig +++ /dev/null @@ -1,263 +0,0 @@ -// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) -// -// Francis Bouvier -// Pierre Tachoire -// -// 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 . - -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; diff --git a/src/runtime/js.zig b/src/runtime/js.zig index f10dcecd..b861aa91 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -664,11 +664,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); @@ -1021,6 +1032,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 @@ -1134,6 +1149,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) = .{}, @@ -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 { id: usize, executor: *Executor, @@ -1575,7 +1619,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 @@ -1588,9 +1631,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 {