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 @@ } + + + + + + + + + + + +