Skip to content

extension: ignore repeated calls to a subscribe unsubscriber - #2138

Open
unknownhackerworld wants to merge 2 commits into
reduxjs:mainfrom
unknownhackerworld:fix/subscribe-double-unsubscribe
Open

unknownhackerworld wants to merge 2 commits into
reduxjs:mainfrom
unknownhackerworld:fix/subscribe-double-unsubscribe

Conversation

@unknownhackerworld

Copy link
Copy Markdown

Fixes #2137

Summary

The function returned by connect().subscribe() in extension/src/pageScript/api/index.ts removed its listener with listenersForId.splice(listenersForId.indexOf(liftedListener), 1). On a second call the listener is already gone, indexOf returns -1, and splice(-1, 1) removes the last entry of listeners[id] instead. That entry is either another subscriber's listener, or the internal rootListener that connect() registers first, which handles PAUSE_RECORDING (and START/STOP with autoPause). After that, the other subscriber stops getting monitor messages, or pausing from the monitor stops reaching the page.

Changes

  • Only call splice when the listener is found (index !== -1), so calling an unsubscribe again is a no-op. This matches how Redux's own store.subscribe unsubscribe behaves.
  • Add extension/test/app/inject/subscribe.spec.ts with two tests:
    • two subscribers, the first one's unsubscribe is called twice, the second one must still receive a message;
    • one subscriber whose unsubscribe is called twice, a PAUSE_RECORDING dispatch must still produce the LIFTED reply from rootListener.
      Each test uses its own instanceId and targets messages with id, so the two connections do not interfere.
  • Add a patch changeset for remotedev-redux-devtools-extension.

Alternatives considered

  • Guarding with a per-subscription isSubscribed flag (the way Redux does it) would also work, but the indexOf check already gives the same result here without extra state.
  • Filtering the array (listeners[id] = listeners[id].filter(...)) would replace the array that the subscribe closure holds on to, so later subscribers and unsubscribes could act on a stale array. Keeping the in-place splice avoids that.

Testing

  • pnpm vitest --run --project extension test/app/inject/subscribe.spec.ts: both tests fail on main (expected "vi.fn()" to be called 1 times, but got 0 times and expected null to match object { type: 'LIFTED', ... }) and pass with the fix.
  • pnpm vitest --run --project extension test/app/inject: 6 files, 24 tests pass.
  • oxlint and oxfmt --check are clean on the changed files.

AI assistance: this bug was found and the fix was drafted with AI tooling in my workflow.

The function returned by connect().subscribe() looked up its listener
with indexOf and passed the result straight to splice. After the first
call the listener is gone, so indexOf returns -1 and splice(-1, 1)
removes the last listener in the array instead: another subscriber's
listener, or the internal listener that handles pause messages from the
monitor (autoPause START/STOP and PAUSE_RECORDING). Only splice when
the listener is found.

Signed-off-by: Allen Joseph G <63872540+unknownhackerworld@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 26, 2026 21:04
@changeset-bot

changeset-bot Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 15f24a4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
remotedev-redux-devtools-extension Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

connect().subscribe: calling unsubscribe twice removes another listener

2 participants