diff --git a/src/browser/EventManagerBase.zig b/src/browser/EventManagerBase.zig
index d736ec2ba..3b782db83 100644
--- a/src/browser/EventManagerBase.zig
+++ b/src/browser/EventManagerBase.zig
@@ -256,22 +256,13 @@ pub fn dispatchDirect(
// Per spec, currentTarget is only set while listeners are being invoked
defer event._current_target = null;
- // Call the property handler (e.g., onmessage) if present
- if (getFunction(handler, &ls.local)) |func| {
- event._current_target = target;
- _ = func.callWithThis(void, target, .{event}) catch |err| {
- log.warn(.event, opts.context, .{ .err = err });
- };
- }
-
- // Call listeners registered via addEventListener
- const list = self.getListeners(target, event._type_string) orelse return;
-
// This is a slightly simplified version of what you'll find in EventManager.
// dispatchPhase. It is simpler because, for direct dispatching, we know
// there's no ancestors and only the single target phase.
- // Track dispatch depth for deferred removal
+ // Track dispatch depth for deferred removal. Bump it *before* the property
+ // handler runs so any listener it removes is deferred (keeping our sentinel
+ // node alive) rather than freed mid-dispatch.
self.dispatch_depth += 1;
defer {
self.dispatch_depth -= 1;
@@ -285,8 +276,28 @@ pub fn dispatchDirect(
}
}
- // Use the last listener in the list as sentinel - listeners added during dispatch will be after it
- const last_node = list.last orelse return;
+ // Snapshot the listener list *before* invoking the property handler. Per
+ // spec the set of listeners is collected at the start of dispatch, so a
+ // listener added while we're dispatching — including one added by the
+ // property handler itself (e.g. onupgradeneeded calling addEventListener) —
+ // must not be invoked for this event.
+ const maybe_list = self.getListeners(target, event._type_string);
+ const sentinel = if (maybe_list) |list| list.last else null;
+
+ // Call the property handler (e.g., onmessage) if present
+ if (getFunction(handler, &ls.local)) |func| {
+ event._current_target = target;
+ _ = func.callWithThis(void, target, .{event}) catch |err| {
+ log.warn(.event, opts.context, .{ .err = err });
+ };
+ }
+
+ // No listeners were registered via addEventListener at dispatch start.
+ const last_node = sentinel orelse return;
+ const list = maybe_list.?;
+
+ // Use the last listener present at dispatch start as sentinel - listeners
+ // added during dispatch will be after it
const last_listener: *Listener = @alignCast(@fieldParentPtr("node", last_node));
// Iterate through the list, stopping after we've encountered the last_listener
diff --git a/src/browser/tests/indexeddb.html b/src/browser/tests/indexeddb.html
index acbcc0f8d..18703f4ec 100644
--- a/src/browser/tests/indexeddb.html
+++ b/src/browser/tests/indexeddb.html
@@ -647,18 +647,21 @@
open.onsuccess = (e) => {
const store = e.target.result.transaction("s", "readonly").objectStore("s");
const seen = [];
+ let sourceIsStore = null;
const req = store.openCursor();
req.onsuccess = () => {
const cursor = req.result;
if (cursor) {
+ if (sourceIsStore === null) sourceIsStore = cursor.source === store;
seen.push([cursor.key, cursor.primaryKey, cursor.value, cursor.direction]);
cursor.continue();
} else {
- state.resolve(seen);
+ state.resolve({ seen, sourceIsStore });
}
};
};
- await state.done((seen) => {
+ await state.done((got) => {
+ const seen = got.seen;
testing.expectEqual(3, seen.length);
testing.expectEqual(1, seen[0][0]); // key
testing.expectEqual(1, seen[0][1]); // primaryKey == key for object store
@@ -666,6 +669,7 @@
testing.expectEqual("next", seen[0][3]); // direction
testing.expectEqual(3, seen[2][0]);
testing.expectEqual("c", seen[2][2]);
+ testing.expectEqual(true, got.sourceIsStore); // cursor.source === the store handle
});
}
@@ -773,6 +777,144 @@
}
+
+
+
+
+
+
+
+
+
+
+
+