mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-16 00:08:59 +00:00
Allow event listener to remove itself or other pending listeners
This commit is contained in:
@@ -39,6 +39,7 @@ page: *Page,
|
|||||||
arena: Allocator,
|
arena: Allocator,
|
||||||
listener_pool: std.heap.MemoryPool(Listener),
|
listener_pool: std.heap.MemoryPool(Listener),
|
||||||
lookup: std.AutoHashMapUnmanaged(usize, std.DoublyLinkedList),
|
lookup: std.AutoHashMapUnmanaged(usize, std.DoublyLinkedList),
|
||||||
|
dispatch_depth: u32 = 0,
|
||||||
|
|
||||||
pub fn init(page: *Page) EventManager {
|
pub fn init(page: *Page) EventManager {
|
||||||
return .{
|
return .{
|
||||||
@@ -46,6 +47,7 @@ pub fn init(page: *Page) EventManager {
|
|||||||
.lookup = .{},
|
.lookup = .{},
|
||||||
.arena = page.arena,
|
.arena = page.arena,
|
||||||
.listener_pool = std.heap.MemoryPool(Listener).init(page.arena),
|
.listener_pool = std.heap.MemoryPool(Listener).init(page.arena),
|
||||||
|
.dispatch_depth = 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,12 +249,27 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
const page = self.page;
|
const page = self.page;
|
||||||
const typ = event._type_string;
|
const typ = event._type_string;
|
||||||
|
|
||||||
|
// Track that we're dispatching to prevent immediate removal
|
||||||
|
self.dispatch_depth += 1;
|
||||||
|
defer {
|
||||||
|
self.dispatch_depth -= 1;
|
||||||
|
// Clean up any marked listeners in this target's list after this phase
|
||||||
|
// We do this regardless of depth to handle cross-target removals correctly
|
||||||
|
self.cleanupMarkedListeners(list);
|
||||||
|
}
|
||||||
|
|
||||||
var node = list.first;
|
var node = list.first;
|
||||||
while (node) |n| {
|
while (node) |n| {
|
||||||
// do this now, in case we need to remove n (once: true or aborted signal)
|
// do this now, in case we need to remove n (once: true or aborted signal)
|
||||||
node = n.next;
|
node = n.next;
|
||||||
|
|
||||||
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||||
|
|
||||||
|
// Skip listeners that were marked for removal
|
||||||
|
if (listener.marked_for_removal) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!listener.typ.eql(typ)) {
|
if (!listener.typ.eql(typ)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -310,8 +327,27 @@ fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target:
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void {
|
fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void {
|
||||||
list.remove(&listener.node);
|
if (self.dispatch_depth > 0) {
|
||||||
self.listener_pool.destroy(listener);
|
// We're in the middle of dispatching, just mark for removal
|
||||||
|
// This prevents invalidating the linked list during iteration
|
||||||
|
listener.marked_for_removal = true;
|
||||||
|
} else {
|
||||||
|
// Safe to remove immediately
|
||||||
|
list.remove(&listener.node);
|
||||||
|
self.listener_pool.destroy(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanupMarkedListeners(self: *EventManager, list: *std.DoublyLinkedList) void {
|
||||||
|
var node = list.first;
|
||||||
|
while (node) |n| {
|
||||||
|
node = n.next;
|
||||||
|
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||||
|
if (listener.marked_for_removal) {
|
||||||
|
list.remove(&listener.node);
|
||||||
|
self.listener_pool.destroy(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, function: js.Function, capture: bool) ?*Listener {
|
fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, function: js.Function, capture: bool) ?*Listener {
|
||||||
@@ -341,6 +377,7 @@ const Listener = struct {
|
|||||||
function: Function,
|
function: Function,
|
||||||
signal: ?*@import("webapi/AbortSignal.zig") = null,
|
signal: ?*@import("webapi/AbortSignal.zig") = null,
|
||||||
node: std.DoublyLinkedList.Node,
|
node: std.DoublyLinkedList.Node,
|
||||||
|
marked_for_removal: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Function = union(enum) {
|
const Function = union(enum) {
|
||||||
|
|||||||
@@ -133,12 +133,10 @@ pub fn getNodePtr(self: *const Inspector, allocator: Allocator, object_id: []con
|
|||||||
// The values context and groupId are not used here
|
// The values context and groupId are not used here
|
||||||
const js_val = unwrapped.value;
|
const js_val = unwrapped.value;
|
||||||
if (js_val.isObject() == false) {
|
if (js_val.isObject() == false) {
|
||||||
std.debug.print("XX-0\n", .{});
|
|
||||||
return error.ObjectIdIsNotANode;
|
return error.ObjectIdIsNotANode;
|
||||||
}
|
}
|
||||||
const Node = @import("../webapi/Node.zig");
|
const Node = @import("../webapi/Node.zig");
|
||||||
return Context.typeTaggedAnyOpaque(*Node, js_val.castTo(v8.Object)) catch {
|
return Context.typeTaggedAnyOpaque(*Node, js_val.castTo(v8.Object)) catch {
|
||||||
std.debug.print("XX-1\n", .{});
|
|
||||||
return error.ObjectIdIsNotANode;
|
return error.ObjectIdIsNotANode;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -319,14 +319,20 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=objectWithoutHandleEvent>
|
<script id=objectWithoutHandleEvent>
|
||||||
// Test object without handleEvent (should be ignored)
|
// Test that registration succeeds even with invalid handlers
|
||||||
|
// (Dispatch behavior differs: spec-compliant browsers throw, some ignore)
|
||||||
const badHandler = { foo: 'bar' };
|
const badHandler = { foo: 'bar' };
|
||||||
let badHandlerTestPassed = false;
|
let registrationSucceeded = false;
|
||||||
window.addEventListener('testbad', badHandler);
|
window.addEventListener('testbad', badHandler);
|
||||||
// Event should not be triggered since there's no valid handler
|
registrationSucceeded = true;
|
||||||
window.dispatchEvent(new Event('testbad'));
|
testing.expectEqual(true, registrationSucceeded);
|
||||||
badHandlerTestPassed = true;
|
|
||||||
testing.expectEqual(true, badHandlerTestPassed);
|
// Test object with handleEvent that's not a function
|
||||||
|
const badHandler2 = { handleEvent: 'not a function' };
|
||||||
|
let registrationSucceeded2 = false;
|
||||||
|
window.addEventListener('testbad2', badHandler2);
|
||||||
|
registrationSucceeded2 = true;
|
||||||
|
testing.expectEqual(true, registrationSucceeded2);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=passiveDetection>
|
<script id=passiveDetection>
|
||||||
@@ -347,3 +353,147 @@
|
|||||||
|
|
||||||
testing.expectEqual(true, passiveSupported);
|
testing.expectEqual(true, passiveSupported);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div id=reentrancy_test></div>
|
||||||
|
<script id=removeOtherListenerDuringDispatch>
|
||||||
|
// Test that removing another listener during dispatch doesn't crash
|
||||||
|
// This reproduces the segfault bug where n.next becomes invalid
|
||||||
|
const reentrancy_el = $('#reentrancy_test');
|
||||||
|
let reentrancy_calls = [];
|
||||||
|
|
||||||
|
const listener1 = () => {
|
||||||
|
reentrancy_calls.push(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const listener2 = () => {
|
||||||
|
reentrancy_calls.push(2);
|
||||||
|
// Remove listener3 while we're still iterating the listener list
|
||||||
|
reentrancy_el.removeEventListener('reentrancy', listener3);
|
||||||
|
};
|
||||||
|
|
||||||
|
const listener3 = () => {
|
||||||
|
reentrancy_calls.push(3);
|
||||||
|
};
|
||||||
|
|
||||||
|
const listener4 = () => {
|
||||||
|
reentrancy_calls.push(4);
|
||||||
|
};
|
||||||
|
|
||||||
|
reentrancy_el.addEventListener('reentrancy', listener1);
|
||||||
|
reentrancy_el.addEventListener('reentrancy', listener2);
|
||||||
|
reentrancy_el.addEventListener('reentrancy', listener3);
|
||||||
|
reentrancy_el.addEventListener('reentrancy', listener4);
|
||||||
|
|
||||||
|
reentrancy_el.dispatchEvent(new Event('reentrancy'));
|
||||||
|
|
||||||
|
// listener3 was removed during dispatch by listener2, so it should not fire
|
||||||
|
// But listener4 should still fire
|
||||||
|
testing.expectEqual(1, reentrancy_calls[0]);
|
||||||
|
testing.expectEqual(2, reentrancy_calls[1]);
|
||||||
|
testing.expectEqual(4, reentrancy_calls[2]);
|
||||||
|
testing.expectEqual(3, reentrancy_calls.length);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id=self_remove_test></div>
|
||||||
|
<script id=removeSelfDuringDispatch>
|
||||||
|
// Test that a listener can remove itself during dispatch
|
||||||
|
const self_remove_el = $('#self_remove_test');
|
||||||
|
let self_remove_calls = [];
|
||||||
|
|
||||||
|
const selfRemovingListener = () => {
|
||||||
|
self_remove_calls.push('self');
|
||||||
|
self_remove_el.removeEventListener('selfremove', selfRemovingListener);
|
||||||
|
};
|
||||||
|
|
||||||
|
const otherListener = () => {
|
||||||
|
self_remove_calls.push('other');
|
||||||
|
};
|
||||||
|
|
||||||
|
self_remove_el.addEventListener('selfremove', selfRemovingListener);
|
||||||
|
self_remove_el.addEventListener('selfremove', otherListener);
|
||||||
|
|
||||||
|
// First dispatch - selfRemovingListener should fire and remove itself
|
||||||
|
self_remove_el.dispatchEvent(new Event('selfremove'));
|
||||||
|
testing.expectEqual('self', self_remove_calls[0]);
|
||||||
|
testing.expectEqual('other', self_remove_calls[1]);
|
||||||
|
testing.expectEqual(2, self_remove_calls.length);
|
||||||
|
|
||||||
|
// Second dispatch - only otherListener should fire
|
||||||
|
self_remove_el.dispatchEvent(new Event('selfremove'));
|
||||||
|
testing.expectEqual('other', self_remove_calls[2]);
|
||||||
|
testing.expectEqual(3, self_remove_calls.length);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id=multi_remove_test></div>
|
||||||
|
<script id=removeMultipleListenersDuringDispatch>
|
||||||
|
// Test removing multiple listeners during dispatch (stress test)
|
||||||
|
const multi_el = $('#multi_remove_test');
|
||||||
|
let multi_calls = [];
|
||||||
|
|
||||||
|
const listeners = [];
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const listener = () => {
|
||||||
|
multi_calls.push(i);
|
||||||
|
// Each even-numbered listener removes the next two listeners
|
||||||
|
if (i % 2 === 0 && listeners[i + 1] && listeners[i + 2]) {
|
||||||
|
multi_el.removeEventListener('multiremove', listeners[i + 1]);
|
||||||
|
multi_el.removeEventListener('multiremove', listeners[i + 2]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
listeners.push(listener);
|
||||||
|
multi_el.addEventListener('multiremove', listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
multi_el.dispatchEvent(new Event('multiremove'));
|
||||||
|
|
||||||
|
// Should see: 0 (removes 1,2), 3 (but 1,2 already removed), 4 (removes 5,6), 7 (but 5,6 already removed), 8 (doesn't remove because listeners[10] doesn't exist), 9 (not removed)
|
||||||
|
// Expected: 0, 3, 4, 7, 8, 9
|
||||||
|
testing.expectEqual(0, multi_calls[0]);
|
||||||
|
testing.expectEqual(3, multi_calls[1]);
|
||||||
|
testing.expectEqual(4, multi_calls[2]);
|
||||||
|
testing.expectEqual(7, multi_calls[3]);
|
||||||
|
testing.expectEqual(8, multi_calls[4]);
|
||||||
|
testing.expectEqual(9, multi_calls[5]);
|
||||||
|
testing.expectEqual(6, multi_calls.length);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id=nested_dispatch_test></div>
|
||||||
|
<script id=nestedDispatchWithRemoval>
|
||||||
|
// Test nested event dispatch with listener removal
|
||||||
|
const nested_el = $('#nested_dispatch_test');
|
||||||
|
let nested_calls = [];
|
||||||
|
|
||||||
|
const inner1 = () => {
|
||||||
|
nested_calls.push('inner1');
|
||||||
|
};
|
||||||
|
|
||||||
|
const inner2 = () => {
|
||||||
|
nested_calls.push('inner2');
|
||||||
|
};
|
||||||
|
|
||||||
|
const outer = () => {
|
||||||
|
nested_calls.push('outer-start');
|
||||||
|
// Dispatch another event in the middle of handling this one
|
||||||
|
nested_el.dispatchEvent(new Event('inner'));
|
||||||
|
nested_calls.push('outer-end');
|
||||||
|
// Remove a listener after the nested dispatch
|
||||||
|
nested_el.removeEventListener('inner', inner2);
|
||||||
|
};
|
||||||
|
|
||||||
|
nested_el.addEventListener('outer', outer);
|
||||||
|
nested_el.addEventListener('inner', inner1);
|
||||||
|
nested_el.addEventListener('inner', inner2);
|
||||||
|
|
||||||
|
nested_el.dispatchEvent(new Event('outer'));
|
||||||
|
|
||||||
|
// Should see outer-start, then both inner listeners, then outer-end
|
||||||
|
testing.expectEqual('outer-start', nested_calls[0]);
|
||||||
|
testing.expectEqual('inner1', nested_calls[1]);
|
||||||
|
testing.expectEqual('inner2', nested_calls[2]);
|
||||||
|
testing.expectEqual('outer-end', nested_calls[3]);
|
||||||
|
|
||||||
|
// Dispatch inner again - inner2 should be gone
|
||||||
|
nested_el.dispatchEvent(new Event('inner'));
|
||||||
|
testing.expectEqual('inner1', nested_calls[4]);
|
||||||
|
testing.expectEqual(5, nested_calls.length);
|
||||||
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user