Severity: P0 · Batch: B2 (useEvents)
Location
src/hooks/useEvents.js:84 (currentHandlers.set(eventName, handlerWithIndex) inside elements.forEach), removal loops at :51-56 and :97-104
Root cause
currentHandlers is keyed by event name only; with N elements, each iteration overwrites the previous element's wrapper. Cleanup calls removeEventListener(event, lastWrapper) on all elements — a no-op for elements 0..N-2.
Failure scenario
Cleanup leaves N-1 live listeners per event; every reactive re-bind (updateEventListeners) stacks an additional handler on all but the last element.
Proposed fix
Key by element+event, e.g. Map<EventTarget, Map<string, Function>>, or store {element, eventName, wrapper} tuples and remove exactly what was added.
Verification recipe
Spec: useEvents([el1, el2, el3], { click: spy }) in a context, run the returned cleanup, dispatch click on el1, assert spy not called. Expected on unmodified code: spy IS called (listener leaked) → confirmed.
Severity: P0 · Batch: B2 (useEvents)
Location
src/hooks/useEvents.js:84(currentHandlers.set(eventName, handlerWithIndex)insideelements.forEach), removal loops at:51-56and:97-104Root cause
currentHandlersis keyed by event name only; with N elements, each iteration overwrites the previous element's wrapper. Cleanup callsremoveEventListener(event, lastWrapper)on all elements — a no-op for elements 0..N-2.Failure scenario
Cleanup leaves N-1 live listeners per event; every reactive re-bind (
updateEventListeners) stacks an additional handler on all but the last element.Proposed fix
Key by element+event, e.g.
Map<EventTarget, Map<string, Function>>, or store{element, eventName, wrapper}tuples and remove exactly what was added.Verification recipe
Spec:
useEvents([el1, el2, el3], { click: spy })in a context, run the returned cleanup, dispatch click onel1, assert spy not called. Expected on unmodified code: spy IS called (listener leaked) → confirmed.