Files
node/test/parallel/test-abortcontroller-internal.js
Aryan 2c07eda791 events: fix weak listener retention overwrite
The weakListeners() retention map in EventTarget stored a single
listener per retainer key. Multiple addEventListener() calls sharing
the same retainer (e.g. two listeners on one target using the same
AbortSignal) silently evicted each other's strong reference, allowing
the evicted listener to be garbage collected before its signal
aborted, so only the most recently registered listener was ever
actually removed on abort.

Changed the retention map to store a Set of listeners per retainer
key instead of a single listener, and added matching cleanup in
remove() so entries are released once their retainer's set is empty.

This also fixes the same class of bug in events.aborted() and the
streams kWeakHandler usage, since both go through the same
EventTarget.prototype.addEventListener() code path.

Fixes: https://github.com/nodejs/node/issues/63954
Signed-off-by: aryan7905 <aryansrivastava354@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/64024
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Aviv Keller <me@aviv.sh>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
2026-08-28 17:42:10 +00:00

52 lines
1.4 KiB
JavaScript

// Flags: --no-warnings --expose-gc --expose-internals
'use strict';
require('../common');
const assert = require('assert');
const {
test,
} = require('node:test');
const {
kWeakHandler,
} = require('internal/event_target');
const { setTimeout: sleep } = require('timers/promises');
// The tests in this file depend on Node.js internal APIs. These are not necessarily
// portable to other runtimes
test('A weak event listener should not prevent gc', async () => {
// If the event listener is weak, however, it should not prevent gc
let ref;
function handler() {}
{
ref = new globalThis.WeakRef(AbortSignal.timeout(1_200_000));
ref.deref().addEventListener('abort', handler, { [kWeakHandler]: {} });
}
await sleep(10);
globalThis.gc();
assert.strictEqual(ref.deref(), undefined);
});
test('two weak listeners with the same retainer should both run on abort', async () => {
// Regression test for https://github.com/nodejs/node/issues/63954
const ac = new AbortController();
let aRan = false;
let bRan = false;
ac.signal.addEventListener('a', () => { aRan = true; }, { [kWeakHandler]: ac });
ac.signal.addEventListener('b', () => { bRan = true; }, { [kWeakHandler]: ac });
await sleep(10);
globalThis.gc();
ac.signal.dispatchEvent(new Event('a'));
ac.signal.dispatchEvent(new Event('b'));
assert.strictEqual(aRan, true);
assert.strictEqual(bRan, true);
});