Description
Describe the bug
The supabase.removeChannel(channel)
method can, under certain conditions, incorrectly remove all channels from the client's internal list instead of just the single specified channel.
This behavior is intermittent and appears to be caused by a race condition. The outcome depends on whether the target channel has fully completed its subscription handshake with the server before removeChannel
is called. The bug is caused by a redundant and faulty filter operation within the removeChannel
method itself.
To Reproduce
The easiest way to consistently reproduce the bug is to create channels without subscribing them, which guarantees the faulty logic will fail.
- Create a Supabase client instance.
- Create multiple channels using
supabase.channel()
. - Log the current channels to see the initial state (3 channels).
- Call
supabase.removeChannel()
on one of the channels. - Log the channels again and observe the error.
// Minimal code to reproduce the issue
const channel1 = supabase.channel('room1');
const channel2 = supabase.channel('room2');
const channel3 = supabase.channel('room3');
// 1. Check initial state
console.log('Channels before removal:', supabase.getChannels());
// EXPECTED AND ACTUAL: Shows an array of 3 channel objects.
// 2. Attempt to remove just one channel
const status = await supabase.removeChannel(channel2);
console.log('Removal status:', status); // "ok" (sometimes returns undefined but that is a separate issue, might PR if I got the time.)
// 3. Check final state
console.log('Channels after removal:', supabase.getChannels());
// EXPECTED: An array with [channel1, channel3]
// ACTUAL: An empty array []
Expected behavior
I expect only the channel passed to supabase.removeChannel()
(in this case, channel2
) to be removed from the list of active channels. The final console.log
should show an array containing channel1
and channel3
.
System information
- OS: Platform-agnostic (reproducible on macOS, Windows, Linux)
- Browser (if applies): Platform-agnostic (reproducible in Chrome, Firefox, and Node.js environments)
- Version of supabase-js: latest ^2.50.0
- Version of Node.js: Any (e.g.,
18.x
)
Why it "Sometimes" Happens (The Race Condition)
The bug stems from this line in the RealtimeClient.ts
removeChannel
method:
this.channels = this.channels.filter((c) => c._joinRef !== channel._joinRef)
The property _joinRef
is only assigned a value after a channel successfully subscribes to the server. Before that, it's undefined
.
-
Scenario A (It Fails): If you call
removeChannel(channel2)
beforechannel2
has finished subscribing, its_joinRef
isundefined
. The filter then comparesc._joinRef !== undefined
for every other unsubscribed channel. This becomesundefined !== undefined
, which isfalse
, causing all unsubscribed channels to be removed. -
Scenario B (It Works by Coincidence): If you call
removeChannel(channel2)
after it has had time to subscribe, its_joinRef
will have a value (e.g.,"1"
). The filter then becomesc._joinRef !== "1"
. This works as expected, because other channels will either have a different_joinRef
orundefined
, both of which satisfy the condition.
The Deeper Issue & The Fix
The root cause is that the channel.unsubscribe()
method (which is called inside removeChannel
) already does the job of correctly removing the channel from the client's list. The extra, faulty filter line in removeChannel
is redundant and is what introduces the race condition.
I am going to make a PR later this day.
Also feel free to checkout and merge my other PR, completely free of change 😜 supabase/cli#3720