From b908b7f7f7e2b5c0286db6b5b686491b6496b099 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Thu, 13 Aug 2026 15:59:03 +0200 Subject: [PATCH 1/2] refactor(realtime)!: internalize RealtimePresence BREAKING CHANGE: RealtimePresence, PresenceOpts, PresenceEvents, PresenceChooser, PresenceOnJoinCallback, PresenceOnLeaveCallback, and the RealtimeChannel.presence field are internal. Registering a callback through channel.presence silently disabled the channel presence streams because the internal forwarders occupied the same single callback slot. Use RealtimeChannel.onPresenceSync/onPresenceJoin/onPresenceLeave and presenceState() instead. The Presence payload class stays public. --- MIGRATION.md | 26 +++++++++++++++++++ .../realtime_client/lib/realtime_client.dart | 2 +- .../lib/src/realtime_channel.dart | 2 ++ .../lib/src/realtime_presence.dart | 17 ++++++++++++ sdk-compliance.yaml | 25 ------------------ 5 files changed, 46 insertions(+), 26 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index d9e327434..7c1f1896b 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -219,6 +219,32 @@ channel.subscribe(const Duration(seconds: 10)); All channel streams complete when the channel closes, so `await for` loops and `onDone` handlers end on their own once the channel is gone. +### `RealtimePresence` is internal + +`RealtimePresence` and its helper types (`PresenceOpts`, `PresenceEvents`, `PresenceChooser`, +`PresenceOnJoinCallback`, `PresenceOnLeaveCallback`) are now `@internal`, along with the +`RealtimeChannel.presence` field. They were presence bookkeeping that leaked into the public API, +and registering a callback through `channel.presence.onJoin(...)` silently disabled the channel's +own presence events, because the channel's forwarders occupied the same single callback slot. + +Everything the class offered is available on the channel: + +```dart +// Before +channel.presence.onJoin((key, current, joined) { /* ... */ }); +channel.presence.onLeave((key, current, left) { /* ... */ }); +channel.presence.onSync(() { /* ... */ }); +final state = channel.presence.state; + +// After +channel.onPresenceJoin.listen((payload) { /* ... */ }); +channel.onPresenceLeave.listen((payload) { /* ... */ }); +channel.onPresenceSync.listen((payload) { /* ... */ }); +final state = channel.presenceState(); +``` + +The `Presence` payload class is unchanged and stays public. + ### Plural enum names singularized A Dart enum type names one value rather than the set, so its name should be singular. Five enums diff --git a/packages/realtime_client/lib/realtime_client.dart b/packages/realtime_client/lib/realtime_client.dart index 47fa2f73a..7cd4e76fb 100644 --- a/packages/realtime_client/lib/realtime_client.dart +++ b/packages/realtime_client/lib/realtime_client.dart @@ -10,6 +10,6 @@ export 'src/constants.dart' SocketState; export 'src/realtime_channel.dart'; export 'src/realtime_client.dart'; -export 'src/realtime_presence.dart'; +export 'src/realtime_presence.dart' show Presence; export 'src/transformers.dart' show PostgresColumn, PostgresType; export 'src/types.dart' hide ChannelFilter, RealtimeListenType; diff --git a/packages/realtime_client/lib/src/realtime_channel.dart b/packages/realtime_client/lib/src/realtime_channel.dart index 9626ce42e..85bb9915a 100644 --- a/packages/realtime_client/lib/src/realtime_channel.dart +++ b/packages/realtime_client/lib/src/realtime_channel.dart @@ -8,6 +8,7 @@ import 'package:meta/meta.dart'; import 'package:realtime_client/realtime_client.dart'; import 'package:realtime_client/src/constants.dart'; import 'package:realtime_client/src/push.dart'; +import 'package:realtime_client/src/realtime_presence.dart'; import 'package:realtime_client/src/retry_timer.dart'; import 'package:realtime_client/src/transformers.dart'; import 'package:realtime_client/src/types.dart'; @@ -22,6 +23,7 @@ class RealtimeChannel { late Push joinPush; late RetryTimer _rejoinTimer; List _pushBuffer = []; + @internal late RealtimePresence presence; @internal late final String broadcastEndpointURL; diff --git a/packages/realtime_client/lib/src/realtime_presence.dart b/packages/realtime_client/lib/src/realtime_presence.dart index 26b2e8f9c..28d02f8ef 100644 --- a/packages/realtime_client/lib/src/realtime_presence.dart +++ b/packages/realtime_client/lib/src/realtime_presence.dart @@ -1,4 +1,5 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:meta/meta.dart'; import 'package:realtime_client/realtime_client.dart'; import 'package:realtime_client/src/types.dart'; @@ -36,20 +37,25 @@ class Presence { String toString() => 'Presence(presenceRef: $presenceRef, payload: $payload)'; } +@internal typedef PresenceChooser = T Function(String key, dynamic presence); +@internal typedef PresenceOnJoinCallback = void Function(String? key, dynamic currentPresences, dynamic newPresences); +@internal typedef PresenceOnLeaveCallback = void Function(String? key, dynamic currentPresences, dynamic newPresences); +@internal class PresenceOpts { final PresenceEvents events; const PresenceOpts({required this.events}); } +@internal class PresenceEvents { final String state; final String diff; @@ -57,6 +63,17 @@ class PresenceEvents { const PresenceEvents({required this.state, required this.diff}); } +/// Internal bookkeeping for the presence state of a [RealtimeChannel]. +/// +/// Not part of the public API: the [onJoin], [onLeave], and [onSync] setters +/// hold a single callback slot each, and the [RealtimePresence] constructor +/// installs the forwarders that feed the channel presence streams through +/// them, so replacing a callback silently disables those streams. +/// +/// To observe presence, listen to [RealtimeChannel.onPresenceSync], +/// [RealtimeChannel.onPresenceJoin], and [RealtimeChannel.onPresenceLeave], +/// and read the current state with [RealtimeChannel.presenceState]. +@internal class RealtimePresence { Map> state = >{}; List> pendingDiffs = []; diff --git a/sdk-compliance.yaml b/sdk-compliance.yaml index 1b1276cb7..2861bae36 100644 --- a/sdk-compliance.yaml +++ b/sdk-compliance.yaml @@ -1769,17 +1769,7 @@ features: - RealtimeChannel.onPresenceJoin - RealtimeChannel.onPresenceLeave supporting_symbols: - - PresenceChooser - PresenceEvent - - PresenceEvents - - PresenceEvents.PresenceEvents - - PresenceEvents.diff - - PresenceEvents.state - - PresenceOnJoinCallback - - PresenceOnLeaveCallback - - PresenceOpts - - PresenceOpts.PresenceOpts - - PresenceOpts.events - RealtimePresenceJoinPayload - RealtimePresenceJoinPayload.RealtimePresenceJoinPayload - RealtimePresenceJoinPayload.currentPresences @@ -1847,21 +1837,6 @@ features: - Presence.payload - Presence.presenceRef - Presence.toString - - RealtimeChannel.presence - - RealtimePresence - - RealtimePresence.RealtimePresence - - RealtimePresence.caller - - RealtimePresence.channel - - RealtimePresence.inPendingSyncState - - RealtimePresence.joinRef - - RealtimePresence.list - - RealtimePresence.onJoin - - RealtimePresence.onLeave - - RealtimePresence.onSync - - RealtimePresence.pendingDiffs - - RealtimePresence.state - - RealtimePresence.syncDiff - - RealtimePresence.syncState - SinglePresenceState - SinglePresenceState.SinglePresenceState - SinglePresenceState.key From 9cd9a02cc1ed018f780c06a1a2a131c70bcde163 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Thu, 13 Aug 2026 16:34:31 +0200 Subject: [PATCH 2/2] refactor(realtime): rename PresenceOpts to PresenceOptions --- MIGRATION.md | 2 +- .../realtime_client/lib/src/realtime_presence.dart | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 7c1f1896b..009508c5e 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -221,7 +221,7 @@ end on their own once the channel is gone. ### `RealtimePresence` is internal -`RealtimePresence` and its helper types (`PresenceOpts`, `PresenceEvents`, `PresenceChooser`, +`RealtimePresence` and its helper types (`PresenceOptions`, `PresenceEvents`, `PresenceChooser`, `PresenceOnJoinCallback`, `PresenceOnLeaveCallback`) are now `@internal`, along with the `RealtimeChannel.presence` field. They were presence bookkeeping that leaked into the public API, and registering a callback through `channel.presence.onJoin(...)` silently disabled the channel's diff --git a/packages/realtime_client/lib/src/realtime_presence.dart b/packages/realtime_client/lib/src/realtime_presence.dart index 28d02f8ef..469311242 100644 --- a/packages/realtime_client/lib/src/realtime_presence.dart +++ b/packages/realtime_client/lib/src/realtime_presence.dart @@ -49,10 +49,10 @@ typedef PresenceOnLeaveCallback = void Function(String? key, dynamic currentPresences, dynamic newPresences); @internal -class PresenceOpts { +class PresenceOptions { final PresenceEvents events; - const PresenceOpts({required this.events}); + const PresenceOptions({required this.events}); } @internal @@ -90,11 +90,11 @@ class RealtimePresence { /// /// `channel` - The RealtimeChannel /// - /// `opts` - The options, for example `PresenceOpts(events: + /// `options` - The options, for example `PresenceOptions(events: /// PresenceEvents(state: 'state', diff: 'diff'))` - RealtimePresence(this.channel, [PresenceOpts? opts]) { + RealtimePresence(this.channel, [PresenceOptions? options]) { final events = - opts?.events ?? + options?.events ?? PresenceEvents(state: 'presence_state', diff: 'presence_diff'); channel.onEvents(events.state, ChannelFilter(), (newState, [_]) {