diff --git a/UPDATING.md b/UPDATING.md index c517ce379..4ed2c8748 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -13,6 +13,7 @@ This guide lists the changes needed to upgrade from one version of Ably to a new - `RestChannel`, `RestChannels`, `RestPresence`, `RestChannelOptions`, `RestHistoryParams` and `RestPresenceParams` have been removed. Use `RealtimeChannel`, `RealtimeChannels`, `RealtimePresence`, `RealtimeChannelOptions`, `RealtimeHistoryParams` and `RealtimePresenceParams`. - `Message.fromEncoded`, `Message.fromEncodedArray`, `PresenceMessage.fromEncoded` and `PresenceMessage.fromEncodedArray` now take a `RealtimeChannelOptions` instead of a `RestChannelOptions`. - `Push` and `PushChannel` no longer accept a `rest` client; they now require a `realtime` client. +- `RealtimeChannels.release()` now returns a `Future`, which completes once the channel has been released. A realtime channel can only be released when it is in the `INITIALIZED`, `DETACHED`, or `FAILED` state. Releasing a channel in any other state no longer detaches it implicitly. Instead, the future completes with an `AblyException` with code 90011 and the channel is not released. To release an attached channel, `await channel.detach()` first and then `await realtime.channels.release(name)`. ## [Upgrading from v1.2.13] diff --git a/example/lib/ui/realtime_sliver.dart b/example/lib/ui/realtime_sliver.dart index dbc1b0e0b..71b1e0a68 100644 --- a/example/lib/ui/realtime_sliver.dart +++ b/example/lib/ui/realtime_sliver.dart @@ -142,7 +142,7 @@ class RealtimeSliver extends HookWidget { TextButton( onPressed: () async { await channel.detach(); - realtime.channels.release(Constants.channelName); + await realtime.channels.release(Constants.channelName); channel = realtime.channels.get(Constants.channelName); setupListeners( connectionState, channelState, connectionId, recoveryKey); diff --git a/lib/src/common/src/channels.dart b/lib/src/common/src/channels.dart index 3e8c9918d..be893fc40 100644 --- a/lib/src/common/src/channels.dart +++ b/lib/src/common/src/channels.dart @@ -40,9 +40,9 @@ abstract class Channels extends Iterable { /// Releases a [RealtimeChannel] object with a specified /// [name], deleting it. /// - /// It also removes any listeners associated with the channel. To release a - /// channel, the [ChannelState] must be `INITIALIZED`, `DETACHED`, or - /// `FAILED`. + /// It also removes any listeners associated with the channel. A realtime + /// channel can only be released when it is in the `INITIALIZED`, + /// `DETACHED`, or `FAILED` state. void release(String name) { _channels.remove(name); } diff --git a/lib/src/platform/src/realtime/realtime_channels.dart b/lib/src/platform/src/realtime/realtime_channels.dart index b6d3585ba..2396850a8 100644 --- a/lib/src/platform/src/realtime/realtime_channels.dart +++ b/lib/src/platform/src/realtime/realtime_channels.dart @@ -19,12 +19,18 @@ class RealtimeChannels extends Channels { /// Releases a [RealtimeChannel] object with a specified [name], deleting it. /// - /// It also removes any listeners associated with the channel. To release a - /// channel, the [ChannelState] must be `INITIALIZED`, `DETACHED`, or - /// `FAILED`. + /// It also removes any listeners associated with the channel. A realtime + /// channel can only be released when it is in the `INITIALIZED`, + /// `DETACHED`, or `FAILED` state. If the channel is in any other state, the + /// returned future completes with an [AblyException] with code 90011 and + /// the channel is not released. Await [RealtimeChannel.detach] before calling + /// `release()` on an attached channel. Releasing a channel that does not + /// exist completes without error. @override - void release(String name) { - realtime.invoke(PlatformMethod.releaseRealtimeChannel, { + Future release(String name) async { + // The check against the channel state is left to the platform SDK, as + // [RealtimeChannel.state] is updated asynchronously and can lag behind it. + await realtime.invoke(PlatformMethod.releaseRealtimeChannel, { TxTransportKeys.channelName: name, }); super.release(name); diff --git a/test/mock_method_call_manager.dart b/test/mock_method_call_manager.dart index affa3cfef..51c7f79ea 100644 --- a/test/mock_method_call_manager.dart +++ b/test/mock_method_call_manager.dart @@ -10,6 +10,7 @@ class MockMethodCallManager { bool isAuthenticated = false; final channels = {}; final publishedMessages = >[]; + PlatformException? releaseRealtimeChannelError; MockMethodCallManager() { final channel = @@ -22,6 +23,7 @@ class MockMethodCallManager { void reset() { channels.clear(); publishedMessages.clear(); + releaseRealtimeChannelError = null; handleCounter = 0; final channel = MethodChannel('io.ably.flutter.plugin', StandardMethodCodec(Codec())); @@ -69,6 +71,9 @@ class MockMethodCallManager { return null; case PlatformMethod.releaseRealtimeChannel: + if (releaseRealtimeChannelError != null) { + throw releaseRealtimeChannelError!; + } return null; default: diff --git a/test/realtime/channels_test.dart b/test/realtime/channels_test.dart index afad0dbd2..771b675a7 100644 --- a/test/realtime/channels_test.dart +++ b/test/realtime/channels_test.dart @@ -1,4 +1,5 @@ import 'package:ably_pubsub_device_flutter/ably_pubsub_device_flutter.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../mock_method_call_manager.dart'; @@ -87,30 +88,57 @@ void main() { group('#release', () { test( - '(RSN4a) Takes one argument, the channel name,' - ' and releases the corresponding channel entity', () { + '(RTS4d) Releasing a channel the platform SDK releases' + ' removes it from the collection', () async { channels ..get('channel-1') ..get('channel-2'); expect(channels.length, 2); - channels.release('channel-1'); + await channels.release('channel-1'); expect(channels.length, 1); expect(channels.exists('channel-1'), false); }); test( - '(RSN4b) Calling release() with a channel name' + '(RTS4c) Calling release() with a channel name' ' that does not correspond to an extant channel' - ' entity must return without error', () { + ' must return without error', () async { channels ..get('channel-1') ..get('channel-2'); expect(channels.length, 2); - channels.release('channel-3'); + await channels.release('channel-3'); expect(channels.length, 2); expect(channels.exists('channel-1'), true); expect(channels.exists('channel-2'), true); }); + + test( + '(RTS4e) If the platform SDK fails to release the channel,' + ' release() completes with its error and the channel is not' + ' removed', () async { + final channel = channels.get('channel-1'); + manager.releaseRealtimeChannelError = PlatformException( + code: '90011', + message: 'Can only release a channel in a state where there is no' + ' possibility of further updates from the server being' + ' received (initialized, detached, or failed).' + ' The current state is attached', + details: ErrorInfo(code: 90011, statusCode: 400), + ); + + await expectLater( + channels.release('channel-1'), + throwsA( + isA() + .having((e) => e.errorInfo?.code, 'errorInfo.code', 90011) + .having((e) => e.errorInfo?.statusCode, 'errorInfo.statusCode', + 400), + ), + ); + expect(channels.exists('channel-1'), true); + expect(channels.get('channel-1'), same(channel)); + }); }); }); }