Skip to content

fix(realtime): remove only the given channel from the client - #1669

Merged
spydon merged 2 commits into
mainfrom
lukasklingsbo/sdk-realtime-remove-channel
Aug 7, 2026
Merged

fix(realtime): remove only the given channel from the client#1669
spydon merged 2 commits into
mainfrom
lukasklingsbo/sdk-realtime-remove-channel

Conversation

@spydon

@spydon spydon commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

RealtimeClient.remove() matched channels on joinRef, which is the empty string until a channel subscribes. Removing one channel that had never joined therefore removed every channel that had never joined.

final a = client.channel('a');
client.channel('b');
client.channel('c');

await client.removeChannel(a);

client.getChannels(); // [] instead of [b, c]

Two consequences beyond the wrong list:

  • The dropped channels are still held by the caller but no longer known to the client, so removeAllChannels() will not unsubscribe them and their rejoin timers stay armed.
  • channels going empty makes the client schedule a disconnect while channels are still in use.

remove() is only ever called from RealtimeChannel's close handler passing this, so it now matches on identity.

Tests

Two cases in the remove group of socket_test.dart, both of which fail against the old implementation:

  • removing one of three never-joined channels leaves the other two.
  • removing one of two channels that share a topic leaves the other.

The existing case did not catch this because it uses mocked channels with distinct joinRef values.

Verification

realtime_client (205 tests) and supabase (134 tests) pass.

Found while auditing for leaks in #1668, which has the rest of that work.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed channel removal so only the selected channel is removed.
    • Preserved other channels when multiple channels share the same topic or have not yet joined.
  • Tests

    • Added coverage for removing channels with duplicate topics and unjoined channels.

`RealtimeClient.remove()` matched channels on `joinRef`, which is the empty
string until a channel subscribes. Removing one channel that had never
joined therefore removed every channel that had never joined.

The dropped channels are still held by the caller but are no longer known
to the client, so `removeAllChannels()` does not unsubscribe them and their
rejoin timers stay armed. An empty list also made the client schedule a
disconnect while channels were still in use.

`remove()` is only ever called by a channel passing itself, so it now
matches on identity.
@spydon
spydon requested a review from a team as a code owner August 7, 2026 09:48
@github-actions github-actions Bot added the realtime This issue or pull request is related to realtime label Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bed66922-ce52-489c-9554-09e29526f17b

📥 Commits

Reviewing files that changed from the base of the PR and between 7a04241 and 5d14474.

📒 Files selected for processing (2)
  • packages/realtime_client/lib/src/realtime_client.dart
  • packages/realtime_client/test/socket_test.dart

📝 Walkthrough

Walkthrough

RealtimeClient.remove now removes the exact channel instance instead of all channels with the same joinRef. Tests cover unjoined channels and duplicate topics.

Changes

Channel removal

Layer / File(s) Summary
Identity-based channel removal and regression coverage
packages/realtime_client/lib/src/realtime_client.dart, packages/realtime_client/test/socket_test.dart
RealtimeClient.remove now compares channel instances by identity. Tests verify that shared unjoined channels and duplicate-topic channels are not removed together.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: dshukertjr, vinzent03

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: removing only the specified realtime channel.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lukasklingsbo/sdk-realtime-remove-channel

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

spydon added a commit that referenced this pull request Aug 7, 2026
Fixes three leaks found by auditing the packages and the example apps
with `leak_tracker`. The tracking itself was a one-off investigation and
is not part of this change.

Closes SDK-1441.

## Leaks fixed

**The `Supabase` singleton pinned the disposed client graph.**
`dispose()` tore everything down but never dropped its references, so
the disposed `SupabaseClient` and everything under it (auth, realtime,
PostgREST, storage, functions, the http clients) stayed reachable from a
static field for the rest of the process.

`client` is now a getter over a nullable backing field, and `dispose()`
clears `_client`, `_supabaseAuth`, `_lifecycleListener`,
`_restoreSessionCancellableOperation`, `_logSubscription` and the
pending lifecycle operation. Clearing happens before teardown, so a step
that throws cannot leave the singleton pinning a half-disposed client,
and `dispose()` returns early when there is nothing to dispose so it is
safe to call more than once.

**`YAJsonIsolate.dispose()` hung forever when the isolate was never
used.** It awaited `_createdIsolate.future`, which only completes inside
`initialize()`, so an instance that was never exercised never finished
disposing and left its `ReceivePort` open. Reachable through
`PostgrestClient` and `FunctionsClient` when they are handed a custom
isolate that never runs.

It now returns early after closing the port when nothing was spawned.
Overlapping `dispose()` calls share a single shutdown future, and
`initialize()`, `decode()` and `encode()` throw a `StateError`
afterwards rather than spawning a replacement isolate onto a closed
receive port and waiting for a reply that never arrives.

This one also blocked the fix above: making the existing widget test
dispose the singleton turned it into a 10 minute timeout until the
isolate teardown was fixed.

**Undisposed `TextEditingController` in the `database_crud` and
`passkeys` examples.** Both rename dialogs built a controller in the
calling method and never disposed it.

Each is now a `_RenameDialog` stateful widget that owns and disposes its
controller. Disposing in the caller does not work: `whenComplete` on the
`showDialog` future fires while the route is still animating out, and
the `TextField` then rebuilds against a disposed controller.

## Tests

- `initialization_test.dart` gains cases asserting the singleton drops
its client reference on dispose and that a second `dispose()` completes.
The first captures the instance before disposing, because
`Supabase.instance` itself refuses to hand out a disposed instance.
- `yet_another_json_isolate` gains cases for disposing an unused
isolate, disposing twice, overlapping disposals, and using the isolate
after disposal. Every disposal in that suite is bounded by a timeout so
a regression fails rather than hanging.
- The existing widget test now tears the singleton down via
`addTearDown`, which also covers the isolate fix in a widget test
context. It needs `tester.runAsync` around both initialize and dispose,
see the caveat below.
- The `MockWidget` stub now cancels its auth subscription in
`dispose()`.

## Verification

- `supabase_flutter`: 65 tests pass.
- `supabase`: 134 tests pass.
- `yet_another_json_isolate`: 27, `postgrest`: 196, `functions_client`:
48, `realtime_client` and `storage_client` all pass.
- `dcm analyze packages` is clean.
- The example fixes were confirmed on an Android emulator against the
local stack while the leak tracking was still wired up: `database_crud`
went from one reported leak to none. `passkeys` needs real platform
passkey support and does not run on an emulator, so its identical fix
comes from inspection.

## Breaking change

`Supabase.instance.client` is a getter rather than a field, so it can no
longer be assigned, and it throws a `StateError` after `dispose()`
instead of returning the disposed client. Reads on an initialized
instance are unchanged.

## Found but not fixed here

**`SupabaseClient.dispose()` cannot complete inside `testWidgets`.** The
constructor eagerly spawns the JSON isolate, and `Isolate.spawn` never
resolves under the fake clock, so consumers writing widget tests have to
wrap both `Supabase.initialize` and `dispose` in `tester.runAsync`.
Making `dispose()` non-blocking would weaken its contract, so it is a
design call rather than a drive-by fix.

The other finding from this audit, `RealtimeClient.remove()` dropping
unrelated channels, is fixed separately in #1669.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Renaming tasks and passkeys now opens a dedicated dialog with the
current name prefilled.
* Saved names are automatically trimmed of leading and trailing spaces.

* **Bug Fixes**
* Improved cleanup and disposal behavior for Supabase and JSON
processing resources.
  * Repeated or early disposal is now handled safely.
  * Attempts to use disposed services now provide clear state errors.
  * Improved cleanup during authentication and widget testing scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@spydon
spydon merged commit e5d003d into main Aug 7, 2026
34 checks passed
@spydon
spydon deleted the lukasklingsbo/sdk-realtime-remove-channel branch August 7, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

realtime This issue or pull request is related to realtime

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants