packages/core/src/state/pending.ts:49-66 in register checks signal?.aborted and rejects the deferred when the caller's signal is already aborted, but then continues to this.entries.set(correlationId, entry) at line 66. The entry now holds a settled deferred, no timer is armed for it, and there is no path that ever removes it from the map. Subsequent resolve/reject/cancel calls for that correlation id no-op because the deferred is already settled, so the entry remains in this.entries for the life of the registry. A long-running session that registers many correlation ids with pre-aborted signals (e.g. cancelled subscribes during reconnect storms) leaks entries proportional to the failure rate.
A second, smaller issue in the same function: the signal.addEventListener("abort", ...) registered at line 56 is { once: true } but is not explicitly removed on normal resolve or timeout. The listener holds a closure over the registry; minor, but worth a removeEventListener in the resolve/reject/timeout paths if the caller's signal outlives the request.
Fix prompt: in packages/core/src/state/pending.ts:49, restructure register so the pre-aborted-signal branch returns the settled deferred without inserting into this.entries. Concretely, move the if (signal?.aborted) { deferred.reject(...); return deferred; } check above the entry construction. For the listener-cleanup side, capture the onAbort reference, register it once, and call signal.removeEventListener("abort", onAbort) from the entry's terminal handlers (resolve, reject, cancel, timeout). Add a unit test that registers 1000 entries with a pre-aborted signal and asserts the registry's internal map size is zero after.
packages/core/src/state/pending.ts:49-66inregistercheckssignal?.abortedand rejects the deferred when the caller's signal is already aborted, but then continues tothis.entries.set(correlationId, entry)at line 66. The entry now holds a settled deferred, no timer is armed for it, and there is no path that ever removes it from the map. Subsequentresolve/reject/cancelcalls for that correlation id no-op because the deferred is already settled, so the entry remains inthis.entriesfor the life of the registry. A long-running session that registers many correlation ids with pre-aborted signals (e.g. cancelled subscribes during reconnect storms) leaks entries proportional to the failure rate.A second, smaller issue in the same function: the
signal.addEventListener("abort", ...)registered at line 56 is{ once: true }but is not explicitly removed on normal resolve or timeout. The listener holds a closure over the registry; minor, but worth aremoveEventListenerin the resolve/reject/timeout paths if the caller's signal outlives the request.Fix prompt: in
packages/core/src/state/pending.ts:49, restructureregisterso the pre-aborted-signal branch returns the settled deferred without inserting intothis.entries. Concretely, move theif (signal?.aborted) { deferred.reject(...); return deferred; }check above the entry construction. For the listener-cleanup side, capture theonAbortreference, register it once, and callsignal.removeEventListener("abort", onAbort)from the entry's terminal handlers (resolve, reject, cancel, timeout). Add a unit test that registers 1000 entries with a pre-aborted signal and asserts the registry's internal map size is zero after.