fix(stream): stop a stale join reply from double-firing onSubscribed - #2018
Closed
bayrakdarerdem wants to merge 1 commit into
Closed
bayrakdarerdem wants to merge 1 commit into
bayrakdarerdem wants to merge 1 commit into
Conversation
When a second subscriber on a topic asks for a wider event filter than the current join covers, widenFilterIfNeeded re-joins with the union of both filters. If this happens before the first join's reply has come back from the server (a very reachable timing: two subscribe() calls made back to back land well within a typical network round trip), sendJoin overwrote subscription.joinRef with the new join's ref but left the old join's entry sitting in pendingReplies. When the server's reply for that superseded join arrives later, handleReply still finds it there and invokes its handler, which is the success/error callback closure from the original sendJoin call. That handler runs subscription.joined = true and fires onSubscribed (or onSubscribeError) for every callback registered on the topic, a second time for one logical subscription. The same applies to a channel rejoin after phx_error/phx_close while the original join was still unanswered, since sendJoin is the single call site both paths share. Fix: before sendJoin overwrites a subscription's joinRef, cancel any pending reply still keyed to the old one, via a small cancelPendingReply helper that clears the timer and removes the map entry without invoking the handler. Added a regression test: two subscribe() calls with different eventTypes fire back to back (so the first join's reply is still pending when the second triggers the widen), the current join is answered normally, and then the superseded join's reply arrives late. Confirmed the test fails without the fix (onSubscribed calls go from 1 to 2 per callback when the stale reply lands) and passes with it. Ran the full suite (1279 passed), biome check, and check-types on the changed files: all clean.
Collaborator
|
Thanks @bayrakdarerdem, this is a real bug with a clear write-up and a good regression test. This repo is a read-only mirror, so we've recreated the fix in our internal monorepo with you credited as co-author on the commit. It ships in the upcoming @opensea/sdk 12.11.1. We added one thing: Closing this PR since the change now lives in the monorepo. Thanks again! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When a second subscriber on a topic asks for a wider event filter than the current join covers, widenFilterIfNeeded re-joins with the union of both filters. If this happens before the first join's reply has come back from the server (very reachable: two subscribe() calls made back to back land well within a normal network round trip), sendJoin overwrote subscription.joinRef with the new join's ref but left the old join's entry sitting in pendingReplies.
When the server's reply for that superseded join arrives later, handleReply still finds it there and invokes its handler, the success/error callback closure from the original sendJoin call. That handler sets subscription.joined = true and fires onSubscribed (or onSubscribeError) for every callback registered on the topic, a second time for what is one logical subscription. The same applies to a channel rejoin after phx_error/phx_close while the original join was still unanswered, since sendJoin is the single call site both paths share.
Fix: before sendJoin overwrites a subscription's joinRef, cancel any pending reply still keyed to the old one, via a small cancelPendingReply helper that clears the timer and removes the map entry without invoking the handler.
Added a regression test: two subscribe() calls with different eventTypes fire back to back so the first join's reply is still pending when the second triggers the widen, the current join is answered normally, then the superseded join's reply arrives late. Confirmed the test fails without the fix (onSubscribed calls go from 1 to 2 per callback when the stale reply lands) and passes with it.
Ran the full suite locally (46/46 in transport.spec.ts, 1279 passed overall), biome check, and check-types: all clean.